19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,26 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_address
from . import test_common
from . import test_customize
from . import test_delivery_controller
from . import test_delivery_ui
from . import test_dynamic_snippet_category
from . import test_ecommerce_access
from . import test_express_checkout_flows
from . import test_fuzzy
from . import test_main_controller
from . import test_product_public_category
from . import test_sale_order
from . import test_sale_process
from . import test_sitemap
from . import test_website_editor
from . import test_website_sale_add_to_cart_snippet
from . import test_website_sale_cart_abandoned
from . import test_website_sale_cart_payment
from . import test_website_sale_cart_popover
from . import test_website_sale_cart_recovery
from . import test_website_sale_cart
from . import test_website_sale_checkout_steps
from . import test_website_sale_cart_abandoned
from . import test_website_sale_cart_notification
from . import test_website_sale_cart_payment
from . import test_website_sale_cart_recovery
from . import test_website_sale_combo_configurator
from . import test_website_sale_gmc
from . import test_website_sale_image
from . import test_website_sale_invoice
from . import test_website_sale_mail
from . import test_website_sale_mail_template
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_show_compare_list_price
from . import test_website_sale_visitor
from . import test_website_sale_product
from . import test_website_editor
from . import test_website_sale_product_configurator
from . import test_website_sale_product_filters
from . import test_website_sale_product_page
from . import test_website_sale_product_template
from . import test_website_sale_reorder_from_portal
from . import test_website_sale_seo
from . import test_website_sale_settings
from . import test_website_sale_shop_redirects
from . import test_website_sale_show_compare_list_price
from . import test_website_sale_snippets
from . import test_website_sale_fiscal_position
from . import test_website_sale_invoice
from . import test_website_sale_visitor
from . import test_website_sale_technical_page
from . import test_website_sequence
from . import test_website_sale_product_ribbon

View file

@ -0,0 +1,154 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from contextlib import contextmanager
import io
from PIL import Image
from odoo.fields import Command
from odoo.tools import lazy
from odoo.addons.delivery.tests.common import DeliveryCommon
from odoo.addons.product.tests.common import ProductCommon
from odoo.addons.http_routing.tests.common import MockRequest as websiteMockRequest
from odoo.addons.website_sale.models.website import (
CART_SESSION_CACHE_KEY,
FISCAL_POSITION_SESSION_CACHE_KEY,
PRICELIST_SESSION_CACHE_KEY,
PRICELIST_SELECTED_SESSION_CACHE_KEY,
)
@contextmanager
def MockRequest(
*args,
sale_order_id=None,
website_sale_current_pl=None,
fiscal_position_id=None,
website_sale_selected_pl_id=None,
**kwargs,
):
with websiteMockRequest(*args, **kwargs) as request:
if sale_order_id is not None:
request.session[CART_SESSION_CACHE_KEY] = sale_order_id
request.cart = lazy(request.website._get_and_cache_current_cart)
if website_sale_current_pl is not None:
request.session[PRICELIST_SESSION_CACHE_KEY] = website_sale_current_pl
request.pricelist = lazy(request.website._get_and_cache_current_pricelist)
if website_sale_selected_pl_id is not None:
request.session[PRICELIST_SELECTED_SESSION_CACHE_KEY] = website_sale_selected_pl_id
if fiscal_position_id is not None:
request.session[FISCAL_POSITION_SESSION_CACHE_KEY] = fiscal_position_id
request.fiscal_position = lazy(request.website._get_and_cache_current_fiscal_position)
yield request
class WebsiteSaleCommon(ProductCommon, DeliveryCommon):
# Not based on SaleCommon as there is no need for SalesTeamCommon nor standard SaleCommon data
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env.company.website_id
if not cls.website:
cls.website = cls.env['website'].create({
'name': 'Test Website',
'company_id': cls.env.company.id,
})
cls.public_user = cls.website.user_id
cls.public_partner = cls.public_user.partner_id
cls.empty_cart = cls.env['sale.order'].create({
'partner_id': cls.partner.id,
'website_id': cls.website.id,
})
cls.cart = cls.env['sale.order'].create({
'partner_id': cls.partner.id,
'website_id': cls.website.id,
'order_line': [
Command.create({
'product_id': cls.product.id,
'product_uom_qty': 5.0,
}),
Command.create({
'product_id': cls.service_product.id,
'product_uom_qty': 12.5,
})
]
})
# Publish tests products
(
cls.product
+ cls.service_product
).website_published = True
cls.pricelist.website_id = cls.website
cls.country_be = cls.quick_ref('base.be')
cls.country_us = cls.quick_ref('base.us')
cls.country_us_state_id = cls.env['ir.model.data']._xmlid_to_res_id('base.state_us_39')
cls.dummy_partner_address_values = {
'street': '215 Vine St',
'city': 'Scranton',
'zip': '18503',
'country_id': cls.country_us.id,
'state_id': cls.country_us_state_id,
'phone': '+1 555-555-5555',
'email': 'admin@yourcompany.example.com',
}
def _create_so(self, **values):
default_values = {
'partner_id': self.partner.id,
'website_id': self.website.id,
'order_line': [
Command.create({
'product_id': self.product.id,
}),
],
}
return self.env['sale.order'].create(dict(default_values, **values))
@classmethod
def _prepare_carrier(cls, product, website_published=True, **values):
""" Override of `delivery` to auto-publish test delivery methods. """
return super()._prepare_carrier(product, website_published=website_published, **values)
@classmethod
def _create_product(cls, **kwargs):
""" Override of `product` to auto-publish test products by default. """
if 'website_published' not in kwargs:
kwargs['website_published'] = True
return super()._create_product(**kwargs)
@classmethod
def _create_public_category(cls, list_vals):
"""Create a hierarchical chain of `public.product.category`.
For example::
# Furnitures / Sofas
self._create_public_category([
{'name': 'Furnitures'}, {'name': 'Sofas'}
])
:return: The created categories.
:rtype: public.product.category
"""
categs = cls.env['product.public.category'].create(list_vals)
for i in range(0, len(categs) - 1):
categs[i].parent_id = categs[i + 1]
return categs
@classmethod
def _create_image(cls, color):
f = io.BytesIO()
Image.new('RGB', (1920, 1080), color).save(f, 'JPEG')
f.seek(0)
return base64.b64encode(f.read())

View file

@ -0,0 +1,83 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo.fields import Command
from odoo.addons.website_sale.controllers.product_feed import ProductFeed
from odoo.addons.product.tests.common import ProductVariantsCommon
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
class WebsiteSaleGMCCommon(ProductVariantsCommon, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ProductFeedController = ProductFeed()
cls.website.enabled_gmc_src = True
cls.gmc_feed = cls.env['product.feed'].create({
'name': "GMC",
'website_id': cls.website.id,
})
# Prepare products
cls.product_template_sofa.list_price = 1000.0
(cls.red_sofa, cls.blue_sofa) = cls.product_template_sofa.product_variant_ids[:2]
cls.red_sofa.default_code = 'SOFA-R'
cls.blue_sofa.product_template_attribute_value_ids.filtered(
lambda v: v.name == 'blue'
).price_extra = 200.0
cls.blanket = cls._create_product(name="Blanket")
combos = cls.env['product.combo'].create([
{
'name': "Sofa Combo",
'combo_item_ids': [
Command.create({'product_id': cls.red_sofa.id}),
Command.create({'product_id': cls.blue_sofa.id})
]
},
{
'name': "Blanket Combo",
'combo_item_ids': [
Command.create({'product_id': cls.blanket.id}),
]
}
])
cls.sofa_bundle = cls._create_product(
name="Sofa + Blanket",
type='combo',
combo_ids=[Command.set(combos.ids)],
list_price=1099.0
)
cls.products = cls.red_sofa + cls.blue_sofa + cls.blanket + cls.sofa_bundle
cls.products.website_published = True
# Prepare pricelists
cls.eur_currency = cls.env.ref('base.EUR')
cls.eur_currency.write({
'active': True,
'rate_ids': [
Command.clear(),
Command.create({'name': time.strftime('%Y-%m-%d'), 'rate': 1.1})
],
})
cls.eur_pricelist = cls._create_pricelist(
name="EUR",
currency_id=cls.eur_currency.id,
selectable=True,
)
def update_items(self, feed=None):
feed = feed or self.gmc_feed
feed = feed.with_context(lang=feed.lang_id.code)
with MockRequest(
feed.env,
website=feed.website_id,
website_sale_current_pl=feed.pricelist_id.id,
):
self.items = feed._prepare_gmc_items()
self.red_sofa_item = self.items[self.red_sofa]
self.blue_sofa_item = self.items[self.blue_sofa]

View file

@ -0,0 +1,691 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from unittest.mock import patch
from werkzeug.exceptions import Forbidden
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestCheckoutAddress(WebsiteSaleCommon):
"""Test the address management part of the checkout process:
* address creation (/shop/address)
* address update (/shop/address)
* address choice (/shop/checkout)
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.country_id = cls.country_be.id
cls.user_portal = cls._create_new_portal_user()
cls.user_internal = cls._create_new_internal_user()
cls.user_internal.partner_id.company_id = cls.env.company
def setUp(self):
super().setUp()
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,
'phone': '+333333333333333',
'address_type': 'delivery',
}
self.default_billing_address_values = dict(
self.default_address_values, address_type='billing',
)
def _get_last_address(self, partner):
""" Useful to retrieve the last created address (shipping or billing) """
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
p.active = True
so = self._create_so(partner_id=p.id)
with MockRequest(self.env, website=self.website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
self.WebsiteSaleController.shop_address_submit(**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.shop_address_submit(**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.company_b, self.company_c = self.env['res.company'].create([
{'name': 'Company A'},
{'name': 'Company B'},
{'name': 'Company C'},
])
self.website.company_id = self.company_b
self.env.user.company_id = self.company_a
self.demo_user = self.user_internal
self.demo_user.company_ids += self.company_b
self.demo_user.company_id = self.company_b
self.demo_partner = self.demo_user.partner_id
self.portal_user = self.user_portal
self.portal_partner = self.portal_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(partner_id=self.demo_partner.id)
website = self.website.with_user(self.demo_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
# 1. Logged in user, new shipping
self.WebsiteSaleController.shop_address_submit(**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/internal user, should not edit name or email address of billing
self.default_address_values['partner_id'] = self.demo_partner.id
self.WebsiteSaleController.shop_address_submit(**self.default_billing_address_values)
self.assertEqual(self.demo_partner.company_id, self.company_b, "Logged in user edited billing (the partner itself) should not get its company modified.")
self.assertNotEqual(self.demo_partner.name, self.default_address_values['name'], "Employee cannot change their name during the checkout process.")
self.assertNotEqual(self.demo_partner.email, self.default_address_values['email'], "Employee cannot change their email during the checkout process.")
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(partner_id=self.website.user_id.partner_id.id)
website = self.website.with_user(self.public_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
# 1. Public user, new billing
self.default_address_values['partner_id'] = -1
self.WebsiteSaleController.shop_address_submit(**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.shop_address_submit(**self.default_billing_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_03_carrier_rate_on_shipping_address_change(self):
""" Test that when a shipping address is changed the price of delivery is recalculated
and updated on the order."""
shipping_partner = self.env['res.partner'].create({
'name': 'dummy',
'parent_id': self.partner.id,
'type': 'delivery',
})
self.cart.write({
'carrier_id': self.carrier.id,
'partner_shipping_id': shipping_partner.id
})
website = self.website.with_user(self.public_user).with_context({})
with (
MockRequest(website.env, website=website, sale_order_id=self.cart.id),
patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value={'success': True, 'price': 10, 'warning_message': ''}
) as rate_shipment_mock
):
# Change a shipping address of the order in the checkout.
shipping_partner2 = shipping_partner.copy()
self.WebsiteSaleController.shop_update_address(
partner_id=shipping_partner2.id, address_type='delivery'
)
self.assertGreaterEqual(
rate_shipment_mock.call_count,
1,
msg="The carrier rate must be recalculated when shipping address is changed.",
)
self.assertEqual(
self.cart.order_line.filtered('is_delivery')[0].price_unit,
10,
msg="The recalculated delivery price must be updated on the order.",
)
def test_04_apply_empty_pl(self):
''' Ensure empty pl code reset the applied pl '''
self._enable_pricelists()
so = self._create_so(partner_id=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")
def test_04_pl_reset_on_login(self):
self._enable_pricelists()
"""Check that after login, the SO pricelist is correctly recomputed."""
test_user = self.env['res.users'].create({
'name': 'Toto',
'login': 'long_enough_password',
'password': 'long_enough_password',
})
pl_with_code = self.env['product.pricelist'].create({
'name': 'EUR_test',
'website_id': self.website.id,
'code': 'EUR_test',
})
self.website.user_id.partner_id.property_product_pricelist = self.pricelist
test_user.partner_id.property_product_pricelist = pl_with_code
public_user_env = self.env(user=self.website.user_id)
so = self._create_so(partner_id=public_user_env.user.partner_id.id)
self.assertEqual(so.pricelist_id, self.pricelist)
with MockRequest(
public_user_env,
website=self.website.with_env(public_user_env),
sale_order_id=so.id,
website_sale_current_pl=so.pricelist_id.id
) as request:
self.assertEqual(request.pricelist, self.pricelist)
order = request.cart
self.assertEqual(order, so)
self.assertEqual(order.pricelist_id, self.pricelist)
order_b = request.website.with_user(test_user)._get_and_cache_current_cart()
self.assertEqual(order, order_b)
self.assertEqual(order_b.pricelist_id, pl_with_code)
# TEST WEBSITE & MULTI COMPANY
def test_05_create_so_with_website_and_multi_company(self):
''' This test ensure that the company_id of the website set on the order
is the same as the env company or the one set on the order.
'''
self._setUp_multicompany_env()
# No company on the SO
so = self._create_so(partner_id=self.demo_partner.id)
self.assertEqual(so.company_id, self.website.company_id)
# Same company on the SO and the env user company but no website
with self.assertRaises(ValueError, msg="Should not be able to create SO with company different than the website company"):
self._create_so(partner_id=self.demo_partner.id, company_id=self.company_a.id)
# Same company on the SO and the website company
so = self._create_so(partner_id=self.demo_partner.id, company_id=self.company_b.id)
self.assertEqual(so.company_id, self.website.company_id)
# Different company on the SO and the env user company
with self.assertRaises(ValueError, msg="Should not be able to create SO with company different than the website company"):
self._create_so(partner_id=self.demo_partner.id, company_id=self.company_c.id)
def test_06_portal_user_address_and_company(self):
''' Same as test_03 but with portal user '''
self._setUp_multicompany_env()
so = self._create_so(partner_id=self.portal_partner.id)
website = self.website.with_user(self.portal_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
# 1. Portal user, new shipping, same with the log in user
self.WebsiteSaleController.shop_address_submit(**self.default_address_values)
new_shipping = self._get_last_address(self.portal_partner)
self.assertTrue(new_shipping.company_id != self.env.user.company_id, "Portal 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. Portal user, edit billing
self.default_address_values['partner_id'] = self.portal_partner.id
self.WebsiteSaleController.shop_address_submit(**self.default_billing_address_values)
# Name cannot be changed if there are issued invoices
self.assertNotEqual(self.portal_partner.name, self.default_address_values['name'], "Portal User should not be able to change the name if they have invoices under their name.")
def test_07_change_fiscal_position(self):
"""
Check that the sale order is updated when you change fiscal position.
Change fiscal position by modifying address during checkout process.
"""
self.env.company.country_id = self.country_us
be_address_POST, nl_address_POST = [
{
'name': 'Test name', 'email': 'test@email.com', 'street': 'test', 'phone': '+333333333333333',
'city': 'test', 'zip': '3000', 'country_id': self.env.ref('base.be').id, 'submitted': 1,
'partner_id': self.partner.id,
'callback': '/shop/checkout',
},
{
'name': 'Test name', 'email': 'test@email.com', 'street': 'test', 'phone': '+333333333333333',
'city': 'test', 'zip': '3000', 'country_id': self.env.ref('base.nl').id, 'submitted': 1,
'partner_id': self.partner.id,
'callback': '/shop/checkout',
},
]
fpos_be, fpos_nl = self.env['account.fiscal.position'].create([
{
'sequence': 1,
'name': 'BE',
'auto_apply': True,
'country_id': self.env.ref('base.be').id,
},
{
'sequence': 2,
'name': 'NL',
'auto_apply': True,
'country_id': self.env.ref('base.nl').id,
},
])
tax_10_incl, tax_20_excl, tax_15_incl = self.env['account.tax'].create([
{'name': 'Tax 10% incl', 'amount': 10, 'price_include_override': 'tax_included'},
{'name': 'Tax 20% excl', 'amount': 20, 'price_include_override': 'tax_excluded', 'fiscal_position_ids': fpos_be},
{'name': 'Tax 15% incl', 'amount': 15, 'price_include_override': 'tax_included', 'fiscal_position_ids': fpos_nl},
])
(tax_20_excl | tax_15_incl).original_tax_ids = tax_10_incl
product = self.env['product.product'].create({
'name': 'Product test',
'list_price': 100,
'website_published': True,
'sale_ok': True,
'taxes_id': [tax_10_incl.id]
})
so = self.env['sale.order'].create({
'partner_id': self.partner.id,
'website_id': self.website.id,
'order_line': [Command.create({
'product_id': product.id,
'name': 'Product test',
})]
})
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 9.09, 100.0]
)
website = self.website.with_user(self.public_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
self.WebsiteSaleController.shop_address_submit(**be_address_POST)
self.assertEqual(
so.fiscal_position_id,
fpos_be,
)
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 18.18, 109.09] # (100 : (1 + 10%)) * (1 + 20%) = 109.09
)
self.WebsiteSaleController.shop_address_submit(**nl_address_POST)
self.assertEqual(
so.fiscal_position_id,
fpos_nl,
)
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 13.64, 104.55] # (100 : (1 + 10%)) * (1 + 15%) = 104.55
)
def test_08_new_user_address_state(self):
''' Test the billing and shipping addresses creation values. '''
self._setUp_multicompany_env()
so = self._create_so(partner_id=self.demo_partner.id)
website = self.website.with_user(self.demo_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
# check the default values
self.assertEqual(self.demo_partner, so.partner_invoice_id)
self.assertEqual(self.demo_partner, so.partner_shipping_id)
# 1. Logged-in user, new shipping
self.WebsiteSaleController.shop_address_submit(**self.default_address_values)
new_shipping = self._get_last_address(self.demo_partner)
msg = "New shipping address should have its type set as 'delivery'"
self.assertTrue(new_shipping.type == 'delivery', msg)
# 2. Logged-in user, new billing
self.WebsiteSaleController.shop_address_submit(**self.default_billing_address_values)
new_billing = self._get_last_address(self.demo_partner)
msg = "New billing should have its type set as 'invoice'"
self.assertTrue(new_billing.type == 'invoice', msg)
self.assertNotEqual(new_billing, self.demo_partner)
# 3. Check that invoice/shipping address of so changed
self.assertEqual(new_billing, so.partner_invoice_id)
self.assertEqual(new_shipping, so.partner_shipping_id)
# 4. Logged-in user, new delivery, use delivery as billing
use_delivery_as_billing = (
self.default_address_values | {'use_delivery_as_billing': 'true'}
)
self.WebsiteSaleController.shop_address_submit(**use_delivery_as_billing)
new_delivery_use_same = self._get_last_address(self.demo_partner)
msg = "New delivery use same should have its type set as 'other'"
self.assertTrue(new_delivery_use_same.type == 'other', msg)
self.assertNotEqual(new_billing, self.demo_partner)
# 5. Check that invoice/shipping address of so changed
self.assertEqual(new_delivery_use_same, so.partner_invoice_id)
self.assertEqual(new_delivery_use_same, so.partner_shipping_id)
# 6. forbid address page opening with wrong partners:
with self.assertRaises(Forbidden):
self.WebsiteSaleController.shop_address_submit(
partner_id=self.env.user.partner_id.id, address_type='billing'
)
with self.assertRaises(Forbidden):
self.WebsiteSaleController.shop_address_submit(
partner_id=self.env.user.partner_id.id, address_type='delivery'
)
def test_09_shop_update_address(self):
self.env['ir.config_parameter'].sudo().set_param('auth_password_policy.minlength', 4)
user = self.env['res.users'].create({
'name': 'test',
'login': 'test',
'password': 'test',
})
user_partner = user.partner_id
partner_company = self.env['res.partner'].create({
'name': 'My company',
'is_company': True,
'child_ids': [Command.link(user_partner.id)],
})
colleague = self.env['res.partner'].create({
'parent_id': partner_company.id,
'name': 'whatever',
})
colleague_shipping = self.env['res.partner'].create({
'parent_id': colleague.id,
'name': 'whatever',
'type': 'delivery',
})
self.assertEqual(partner_company.commercial_partner_id, partner_company)
self.assertEqual(user_partner.commercial_partner_id, partner_company)
invoicing, shipping, bad_invoicing, bad_shipping = self.env['res.partner'].create([
{
'name': 'Invoicing',
'street': '215 Vine St',
'city': 'Scranton',
'zip': '18503',
'country_id': self.country_us.id,
'state_id': self.country_us_state_id,
'phone': '+1 555-555-5555',
'email': 'admin@yourcompany.example.com',
'type': 'invoice',
'parent_id': user_partner.id,
},
{
'name': 'Shipping',
'street': '215 Vine St',
'city': 'Scranton',
'zip': '18503',
'country_id': self.country_us.id,
'state_id': self.country_us_state_id,
'phone': '+1 555-555-5555',
'email': 'admin@yourcompany.example.com',
'type': 'delivery',
'parent_id': user_partner.id,
},
{
'name': 'Invalid billing', # missing email
'street': '215 Vine St',
'city': 'Scranton',
'zip': '18503',
'country_id': self.country_us.id,
'state_id': self.country_us_state_id,
'phone': '+1 555-555-5555',
'type': 'invoice',
'parent_id': user_partner.id,
},
{
'name': 'Invalid Shipping',
'type': 'delivery',
'parent_id': user_partner.id,
},
])
so = self._create_so(partner_id=user_partner.id)
self.assertNotEqual(so.partner_shipping_id, shipping)
self.assertNotEqual(so.partner_invoice_id, invoicing)
website = self.website.with_user(user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id):
self.assertFalse(colleague._can_be_edited_by_current_customer(order_sudo=so))
# Invalid addresses unaccessible to current customer
with self.assertRaises(Forbidden):
# cannot use contact type addresses
self.WebsiteSaleController.shop_update_address(partner_id=colleague.id)
with self.assertRaises(Forbidden):
# unrelated partner
self.WebsiteSaleController.shop_update_address(partner_id=self.env.user.partner_id.id)
# Good addresses
self.WebsiteSaleController.shop_update_address(
partner_id=colleague_shipping.id, address_type='delivery')
self.assertEqual(so.partner_shipping_id, colleague_shipping)
self.WebsiteSaleController.shop_update_address(
partner_id=shipping.id, address_type='delivery',
)
self.assertEqual(so.partner_shipping_id, shipping)
self.WebsiteSaleController.shop_update_address(
partner_id=invoicing.id, address_type='billing',
)
self.assertEqual(so.partner_shipping_id, shipping)
self.assertEqual(so.partner_invoice_id, invoicing)
# Using invalid addresses --> change and the customer is forced to update the address
self.WebsiteSaleController.shop_update_address(
partner_id=bad_invoicing.id, address_type='billing')
self.assertEqual(so.partner_invoice_id, bad_invoicing)
redirection = self.WebsiteSaleController._check_addresses(so)
self.assertTrue(redirection is not None)
self.assertEqual(redirection.location, f'/shop/address?partner_id={bad_invoicing.id}&address_type=billing')
# reset to valid one
self.WebsiteSaleController.shop_update_address(
partner_id=invoicing.id, address_type='billing',
)
self.WebsiteSaleController.shop_update_address(
partner_id=bad_shipping.id, address_type='delivery')
self.assertEqual(so.partner_shipping_id, bad_shipping)
redirection = self.WebsiteSaleController._check_addresses(so)
self.assertTrue(redirection is not None)
self.assertEqual(redirection.location, f'/shop/address?partner_id={bad_shipping.id}&address_type=delivery')
# reset to valid one
self.WebsiteSaleController.shop_update_address(
partner_id=shipping.id, address_type='delivery',
)
# Using commercial partner address
self.WebsiteSaleController.shop_update_address(
partner_id=partner_company.id, address_type='billing',
)
self.assertEqual(so.partner_invoice_id, partner_company)
self.WebsiteSaleController.shop_update_address(
partner_id=partner_company.id, address_type='delivery',
)
self.assertEqual(so.partner_shipping_id, partner_company)
def test_10_addresses_updates(self):
# TODO dispatch test to sale & account
partner_company = self.env['res.partner'].create({
'name': 'My company',
'is_company': True,
'child_ids': [
Command.create({
'name': 'partner_1',
}),
Command.create({
'name': 'partner_2',
}),
Command.create({
'name': 'partner_3',
}),
],
})
partner_1, _partner_2, _partner_3 = partner_company.child_ids
self.assertTrue(partner_company.can_edit_vat())
self.assertTrue(partner_company._can_edit_country())
self.assertTrue(all(not p.can_edit_vat() for p in partner_company.child_ids))
self.assertTrue(all(p._can_edit_country() for p in partner_company.child_ids))
dumb_product = self.env['product.product'].create({'name': 'test'})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner_company.id,
'invoice_line_ids': [
Command.create({
'product_id': dumb_product.id,
})
],
})
invoice.action_post()
self.assertEqual(invoice.state, 'posted')
self.assertFalse(partner_company.can_edit_vat())
self.assertFalse(partner_company._can_edit_country())
self.assertTrue(all(p._can_edit_country() for p in partner_company.child_ids))
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner_1.id,
'invoice_line_ids': [
Command.create({
'product_id': dumb_product.id,
})
],
})
invoice.action_post()
self.assertFalse(partner_1._can_edit_country())
def test_11_payment_term_when_address_change(self):
"""Make sure the expected payment terms are set on ecommerce orders"""
self.portal_user = self.user_portal
self.portal_partner = self.portal_user.partner_id
self.assertFalse(self.portal_partner.property_payment_term_id)
so = self._create_so(partner_id=self.portal_partner.id)
self.assertTrue(so.payment_term_id, "A payment term should be set by default on the sale order")
website = self.website.with_user(self.portal_user).with_context({})
with MockRequest(website.env, website=website) as req:
req.httprequest.method = "POST"
self.default_address_values['partner_id'] = self.portal_partner.id
self.WebsiteSaleController.shop_address_submit(**self.default_billing_address_values)
self.assertTrue(so.payment_term_id, "A payment term should still be set on the sale order")
so.website_id = False
so._compute_payment_term_id()
self.assertFalse(so.payment_term_id, "The website default payment term should not be set on a sale order not coming from the website")
def test_12_recompute_taxes_on_address_change(self):
self.env.company.country_id = self.env.ref('base.us')
fpos_be = self.env['account.fiscal.position'].create({
'name': "Fiscal Position BE",
'auto_apply': True,
'country_id': self.country_id,
})
tax_15_incl, tax_0 = self.env['account.tax'].create([
{
'name': "15% excl",
'amount': 15,
'price_include_override': 'tax_included',
'fiscal_position_ids': fpos_be.ids,
},
{
'name': "0%",
'amount': 0,
'fiscal_position_ids': fpos_be.ids,
},
])
tax_0.original_tax_ids = tax_15_incl
self.product.taxes_id = [Command.set(tax_15_incl.ids)]
self.partner.country_id = self.country_id
cart = self.empty_cart
cart.order_line = [Command.create({'product_id': self.product.id})]
amount_untaxed = cart.amount_untaxed
self.assertEqual(cart.fiscal_position_id, fpos_be)
self.assertEqual(cart.order_line.tax_ids, tax_0)
self.partner.country_id = self.env.company.country_id
self.assertNotEqual(cart.fiscal_position_id, fpos_be)
self.assertEqual(cart.order_line.tax_ids, tax_15_incl)
self.assertEqual(cart.amount_untaxed, amount_untaxed, "Untaxed amount should not change")
cart.action_confirm()
self.partner.country_id = self.country_id
self.assertEqual(
cart.order_line.tax_ids, tax_15_incl,
"Tax should no longer change after order confirmation",
)
def test_imported_user_with_trailing_name_can_checkout(self):
"""Ensure that an imported user with trailing spaces in their name can complete checkout without error."""
imported_user = self.env['res.users'].create({
'name': 'Imported User ', # trailing space
'login': 'imported_user',
'email': 'imported@example.com',
})
imported_partner = imported_user.partner_id
so = self._create_so(partner_id=imported_partner.id)
website = self.website.with_user(imported_user).with_context({})
with MockRequest(website.env, website=website, sale_order_id=so.id) as req:
req.httprequest.method = "POST"
values = {
'name': 'Imported User', # trimmed input
'email': 'imported@example.com',
'street': '123 Some Street',
'city': 'Cityville',
'zip': '12345',
'country_id': self.country_id,
'phone': '+33123456789',
'partner_id': imported_partner.id,
'address_type': 'delivery',
'use_delivery_as_billing': 'true',
}
res = self.WebsiteSaleController.shop_address_submit(**values).data
self.assertIsNotNone(json.loads(res).get('redirectUrl'), "We should get a 'redirectUrl' in the response")

View file

@ -0,0 +1,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWSaleCommon(WebsiteSaleCommon):
def test_common(self):
self.assertEqual(self.env.company, self.website.company_id)
self.assertEqual(self.pricelist.currency_id, self.env.company.currency_id)

View file

@ -1,53 +1,56 @@
# 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.fields import Command
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.addons.sale.tests.product_configurator_common import TestProductConfiguratorCommon
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
@tagged('post_install', '-at_install')
class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
class TestCustomize(HttpCaseWithUserDemo, HttpCaseWithUserPortal, TestProductConfiguratorCommon, HttpCaseWithWebsiteUser):
def setUp(self):
super().setUp()
self.env.company.country_id = self.env.ref('base.us')
# create a template
product_template = self.env['product.template'].create({
'name': 'Test Product',
'is_published': True,
'list_price': 750,
})
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.company.country_id = cls.env.ref('base.us')
tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 10})
product_template.taxes_id = tax
product_attribute = self.env['product.attribute'].create({
product_attribute = cls.env['product.attribute'].create({
'name': 'Legs',
'visibility': 'visible',
'sequence': 10,
'value_ids': [
Command.create({
'name': 'Steel - Test',
'sequence': 1,
}),
Command.create({
'name': 'Aluminium',
'sequence': 2,
}),
]
})
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,
# create a template
product_template = cls.env['product.template'].create({
'name': 'Test Product',
'is_published': True,
'list_price': 750,
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)]
})
],
})
# 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])]
}])
tax = cls.env['account.tax'].create({'name': "Test tax", 'amount': 10})
product_template.taxes_id = tax
# 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 = cls.env['product.template.attribute.value'].search([
('product_tmpl_id', '=', product_template.id),
])
for ptav in product_template_attribute_values:
if ptav.name == "Steel - Test":
@ -55,114 +58,42 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
else:
ptav.price_extra = 50.4
# Update the pricelist currency regarding env.company_id currency_id in case company has changed currency with COA installation.
website = self.env['website'].get_current_website()
pricelist = website.get_current_pricelist()
pricelist.write({'currency_id': self.env.company.currency_id.id})
# Ensure that no pricelist is available during the test.
# This ensures that tours which triggers on the amounts will run properly, and that the
# currency will be the company currency.
cls.env['product.pricelist'].action_archive()
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(self.env['website'].get_client_action_url('/shop?search=Test Product'), 'shop_customize', login="admin", timeout=120)
def test_01_admin_shop_custom_attribute_value_tour(self):
self.env.user.write(
{'group_ids': [Command.link(self.env.ref('product.group_product_pricelist').id)]}
)
self.env['product.pricelist'].create({
'name': 'Custom pricelist (TEST)',
'sequence': 4,
'item_ids': [(0, 0, {
'base': 'list_price',
'applied_on': '1_product',
'product_tmpl_id': self.product_product_custo_desk.id,
'price_discount': 20,
'min_quantity': 2,
'compute_price': 'formula'
})]
})
self.start_tour("/", 'a_shop_custom_attribute_value', 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,
self.env['product.pricelist'].create({
'name': 'Base Pricelist',
'item_ids': [Command.create({
'base': 'list_price',
'applied_on': '1_product',
'product_tmpl_id': self.product_product_custo_desk.id,
'price_discount': 20,
'min_quantity': 2,
'compute_price': 'formula',
})],
})
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")
@ -170,41 +101,32 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
""" 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',
'value_ids': [
Command.create({'name': "Dynamic Value 1"}),
Command.create({'name': "Dynamic Value 2"}),
]
})
# 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': 10,
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)]
})
]
})
# 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 = 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":
@ -226,39 +148,29 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
product_attribute = self.env['product.attribute'].create({
'name': "My Attribute",
'create_variant': 'always',
'value_ids': [
Command.create({'name': "My Value 1"}),
Command.create({'name': "My Value 2"}),
Command.create({'name': "My Value 3"}),
],
})
# 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,
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)]
}),
],
})
# 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 = 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
@ -278,57 +190,47 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
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,
'value_ids': [Command.create({'name': "No Variant Value"})],
})
# create the template
product_template = self.env['product.template'].create({
'name': 'Test Product 3',
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute_no_variant.id,
'value_ids': [Command.set(product_attribute_no_variant.value_ids.ids)]
})
]
})
# 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
product_template.attribute_line_ids.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.execute()
self.start_tour(self.env['website'].get_client_action_url('/shop?search=Test Product'), 'shop_list_view_b2c', login="admin")
sol = self.env['sale.order.line'].search([
('product_id', '=', product_template.product_variant_id.id)
])
self.assertTrue(sol)
self.assertEqual(
sol.product_no_variant_attribute_value_ids,
product_template.attribute_line_ids.product_template_value_ids
)
def test_07_editor_shop(self):
self.env["product.pricelist"].create({
"name": "EUR Pricelist",
"selectable": True,
"website_id": self.env.ref("website.default_website").id,
"country_group_ids": [(4, self.env.ref('base.europe').id)],
"sequence": 3,
"currency_id": self.env.ref("base.EUR").id,
})
self.start_tour("/", 'shop_editor', login="admin")
self.env.user.write(
{'group_ids': [Command.link(self.env.ref('product.group_product_pricelist').id)]}
)
self.env['product.pricelist'].create([
{'name': 'Base Pricelist', 'selectable': True},
{'name': 'Other Pricelist', 'selectable': True}
])
self.start_tour("/", 'shop_editor', login="website_user")
def test_08_portal_tour_archived_variant_multiple_attributes(self):
"""The goal of this test is to make sure that an archived variant with multiple
@ -337,78 +239,52 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
Using "portal" to have various users in the tests.
"""
attribute_1, attribute_2, attribute_3 = self.env['product.attribute'].create([
attributes = self.env['product.attribute'].create([
{
'name': 'Size',
'create_variant': 'always',
'value_ids': [
Command.create({'name': 'Large'}),
Command.create({'name': 'Small'}),
],
},
{
'name': 'Color',
'create_variant': 'always',
'value_ids': [
Command.create({'name': 'White'}),
Command.create({'name': 'Black'}),
],
},
{
'name': 'Brand',
'create_variant': 'always',
'value_ids': [
Command.create({'name': 'Brand A'}),
Command.create({'name': 'Brand B'}),
],
},
])
attribute_values = self.env['product.attribute.value'].create([
{
'name': 'Large',
'attribute_id': attribute_1.id,
'sequence': 1,
},
{
'name': 'Small',
'attribute_id': attribute_1.id,
'sequence': 2,
},
{
'name': 'White',
'attribute_id': attribute_2.id,
'sequence': 1,
},
{
'name': 'Black',
'attribute_id': attribute_2.id,
'sequence': 2,
},
{
'name': 'Brand A',
'attribute_id': attribute_3.id,
'sequence': 1,
},
{
'name': 'Brand B',
'attribute_id': attribute_3.id,
'sequence': 2,
},
])
product_template = self.env['product.template'].create({
'name': 'Test Product 2',
'is_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set(attribute.value_ids.ids)],
}) for attribute in attributes
],
})
self.env['product.template.attribute.line'].create([
{
'attribute_id': attribute_1.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, attribute_values.filtered(lambda v: v.attribute_id == attribute_1).ids)],
},
{
'attribute_id': attribute_2.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, attribute_values.filtered(lambda v: v.attribute_id == attribute_2).ids)],
},
{
'attribute_id': attribute_3.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, attribute_values.filtered(lambda v: v.attribute_id == attribute_3).ids)],
},
])
product_template.product_variant_ids[-1].active = False
# Archive (Small, Black, Brand B) variant
combination_to_archive = product_template.attribute_line_ids.product_template_value_ids.filtered(
lambda ptav: ptav.product_attribute_value_id.name in ('Small', 'Black', 'Brand B')
)
variant_to_archive = product_template._get_variant_for_combination(
combination_to_archive
)
self.assertTrue(variant_to_archive)
variant_to_archive.action_archive()
self.assertFalse(variant_to_archive.active)
self.start_tour("/", 'tour_shop_archived_variant_multi', login="portal")
@ -419,41 +295,127 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
Using "portal" to have various users in the tests.
"""
attribute_1 = self.env['product.attribute'].create([
{
'name': 'Size',
'create_variant': 'always',
'display_type': 'pills',
},
])
attribute_values = self.env['product.attribute.value'].create([
{
'name': 'Large',
'attribute_id': attribute_1.id,
'sequence': 1,
},
{
'name': 'Small',
'attribute_id': attribute_1.id,
'sequence': 2,
},
])
product_template = self.env['product.template'].create({
'name': 'Test Product 2',
'is_published': True,
attribute_size = self.env['product.attribute'].create({
'name': 'Size',
'create_variant': 'always',
'display_type': 'pills',
'value_ids': [
Command.create({'name': 'Large'}),
Command.create({'name': 'Small'}),
],
})
self.env['product.template.attribute.line'].create([
{
'attribute_id': attribute_1.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, attribute_values.ids)],
},
])
self.env['product.template'].create({
'name': 'Test Product 2',
'is_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute_size.id,
'value_ids': [Command.set(attribute_size.value_ids.ids)],
})
],
})
self.start_tour("/", 'test_09_pills_variant', login="portal")
def test_10_shop_editor_set_product_ribbon(self):
def test_10_multi_checkbox_attribute(self):
attribute = self.env['product.attribute'].create([
{
'name': 'Options',
'create_variant': 'no_variant',
'display_type': 'multi',
'value_ids': [
Command.create({
'name': 'Option 1',
'default_extra_price': 1,
'sequence': 1,
}),
Command.create({
'name': 'Option 2',
'sequence': 2,
}),
Command.create({
'name': 'Option 3',
'default_extra_price': 3,
'sequence': 3,
}),
Command.create({
'name': 'Option 4',
'sequence': 4,
}),
],
},
])
product_template = self.env['product.template'].create({
'name': 'Product Multi',
'is_published': True,
'list_price': 750,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set(attribute.value_ids.ids)],
}),
],
})
# set an extra price for free attribute values on the product (nothing is free)
self.env['product.template.attribute.value'].search([
('product_tmpl_id', '=', product_template.id),
('price_extra', '=', 0),
]).price_extra = 2
# set an exclusion between option 1 and option 3
self.env['product.template.attribute.value'].search([
('product_tmpl_id', '=', product_template.id),
('price_extra', '=', 1),
]).exclude_for = [
Command.create({
'product_tmpl_id': product_template.id,
'value_ids': [Command.set(
self.env['product.template.attribute.value'].search([
('product_tmpl_id', '=', product_template.id),
('price_extra', '=', 3)
]).ids
)],
}),
]
self.start_tour("/", 'tour_shop_multi_checkbox', login="portal")
def test_11_shop_editor_set_product_ribbon(self):
self.start_tour("/", 'shop_editor_set_product_ribbon', login="admin")
def test_12_multi_checkbox_attribute_single_value(self):
attribute = self.env['product.attribute'].create([
{
'name': 'Toppings',
'create_variant': 'no_variant',
'display_type': 'multi',
'value_ids': [(0, 0, {'name': 'cheese'})],
},
])
self.env['product.template'].create({
'name': 'Burger',
'is_published': True,
'list_price': 750,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [(6, 0, attribute.value_ids.ids)],
}),
],
})
self.start_tour("/", 'tour_shop_multi_checkbox_single_value', login="website_user")
def test_shop_editor_no_alternative_products_visibility(self):
product_no_alternative = self.env['product.template'].create({
'name': 'product_without_alternative',
'is_published': True,
})
product_with_alternative = self.env['product.template'].create({
'name': 'product_with_alternative',
'is_published': True,
'alternative_product_ids': product_no_alternative.ids,
})
product_no_alternative.set_sequence_top()
product_with_alternative.set_sequence_top()
self.start_tour('/', 'shop_editor_no_alternative_products_visibility_tour', login="admin")

View file

@ -0,0 +1,82 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.website_sale.controllers.delivery import Delivery
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleDeliveryController(PaymentCommon, WebsiteSaleCommon):
def setUp(self):
super().setUp()
self.Controller = Delivery()
# test that changing the carrier while there is a pending transaction raises an error
def test_controller_change_carrier_when_transaction(self):
website = self.website.with_env(self.env)
self.empty_cart.transaction_ids = self._create_transaction(flow='redirect', state='pending')
with MockRequest(website.env, website=website, sale_order_id=self.empty_cart.id) as request, self.assertRaises(UserError):
request.cart = self.empty_cart
self.Controller.shop_set_delivery_method(dm_id=self.free_delivery.id)
# test that changing the carrier while there is a draft transaction doesn't raise an error
def test_controller_change_carrier_when_draft_transaction(self):
website = self.website.with_env(self.env)
self.empty_cart.transaction_ids = self._create_transaction(flow='redirect', state='draft')
with MockRequest(website.env, website=website, sale_order_id=self.empty_cart.id):
self.Controller.shop_set_delivery_method(dm_id=self.free_delivery.id)
def test_available_methods(self):
self.env['delivery.carrier'].search([]).action_archive()
self.product_delivery_poste = self.env['product.product'].create({
'name': 'The Poste',
'type': 'service',
'categ_id': self.env.ref('delivery.product_category_deliveries').id,
'sale_ok': False,
'purchase_ok': False,
'list_price': 20.0,
})
self.env['delivery.carrier'].create([
{
'name': 'Over 300',
'delivery_type': 'base_on_rule',
'product_id': self.product_delivery_poste.id,
'website_published': True,
'price_rule_ids': [
Command.create({
'operator': '>=',
'max_value': 300,
'variable': 'price',
}),
],
}, {
'name': 'Under 300',
'delivery_type': 'base_on_rule',
'product_id': self.product_delivery_poste.id,
'website_published': True,
'price_rule_ids': [
Command.create({
'operator': '<',
'max_value': 300,
'variable': 'price',
}),
],
}, {
'name': 'No rules',
'delivery_type': 'base_on_rule',
'product_id': self.product_delivery_poste.id,
'website_published': True,
}, {
'name': 'Fixed',
'product_id': self.product_delivery_poste.id,
'website_published': True,
},
])
self.assertEqual(
self.empty_cart._get_delivery_methods().mapped('name'), ['Under 300', 'Fixed']
)

View file

@ -0,0 +1,75 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo.fields import Command
@odoo.tests.tagged('post_install', '-at_install')
class TestUi(odoo.tests.HttpCase):
def test_01_free_delivery_when_exceed_threshold(self):
if self.env['ir.module.module']._get('payment_custom').state != 'installed':
self.skipTest("Transfer provider is not installed")
transfer_provider = self.env.ref('payment.payment_provider_transfer')
transfer_provider.write({
'state': 'enabled',
'is_published': True,
})
transfer_provider._transfer_ensure_pending_msg_is_set()
# Avoid Shipping/Billing address page
self.env.ref('base.partner_admin').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',
})
self.env['product.product'].create({
'name': 'Office Chair Black TEST',
'list_price': 12.50,
})
self.env.ref("delivery.free_delivery_carrier").write({
'name': 'Delivery Now Free Over 10',
'fixed_price': 2,
'free_over': True,
'amount': 10,
})
self.product_delivery_poste = self.env['product.product'].create({
'name': 'The Poste',
'type': 'service',
'categ_id': self.env.ref('delivery.product_category_deliveries').id,
'sale_ok': False,
'purchase_ok': False,
'list_price': 20.0,
})
self.carrier = self.env['delivery.carrier'].create({
'name': 'The Poste',
'sequence': 9999, # ensure last to load price async
'fixed_price': 20.0,
'delivery_type': 'base_on_rule',
'product_id': self.product_delivery_poste.id,
'website_published': True,
'price_rule_ids': [
Command.create({
'max_value': 5,
'list_base_price': 20,
}),
Command.create({
'operator': '>=',
'max_value': 5,
'list_base_price': 50,
}),
Command.create({
'operator': '>=',
'max_value': 300,
'variable': 'price',
'list_base_price': 0,
}),
]
})
self.start_tour("/", 'check_free_delivery', login="admin")

View file

@ -0,0 +1,59 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import tagged
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestDynamicSnippetCategory(WebsiteSaleCommon):
def setUp(self):
super().setUp()
Category = self.env['product.public.category']
self.category1, self.category2, self.category3 = Category.create([
{'name': "Published Category"},
{'name': "Published Category 2"},
{'name': "Unpublished Category"},
])
self.child_category = Category.create({
'name': "Child category",
'parent_id': self.category1.id,
})
self.env['product.template'].create({
'name': "Test Product",
'public_categ_ids': [
Command.link(self.category1.id),
Command.link(self.category2.id),
Command.link(self.child_category.id),
],
'website_published': True,
})
self.website_sale = WebsiteSale()
self.website = self.website.with_user(self.env.ref('base.user_admin'))
def test_snippet_categories_returns_only_published_and_with_children(self):
categories = self.env['product.public.category'].get_available_snippet_categories(
self.website.id,
)
category_ids = [c['id'] for c in categories]
self.assertIn(self.category1.id, category_ids)
def test_set_category_image(self):
"""Test setting a cover image via JSON-RPC route"""
attachment = self.env['ir.attachment'].create({
'name': "test.png",
'datas': 'iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAJElEQVQI'
'mWP4/b/qPzbM8Pt/1X8GBgaEAJTNgFcHXqOQMV4dAMmObXXo1/BqAAAA'
'AElFTkSuQmCC',
'public': True,
})
with MockRequest(self.website.env, website=self.website):
self.website_sale.set_category_image(self.category1.id, attachment.id)
self.assertEqual(
self.category1.cover_image,
attachment.datas,
"Cover image should match the uploaded attachment",
)

View file

@ -0,0 +1,116 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestEcommerceAccess(HttpCaseWithUserDemo, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.filled_category, cls.empty_category = cls.env['product.public.category'].create([
{'name': 'Category with Product', 'website_id': cls.website.id},
{'name': 'Empty Category', 'website_id': cls.website.id},
])
cls.env['product.public.category'].create([
{
'name': 'Has Products',
'parent_id': cls.filled_category.id,
'website_id': cls.website.id,
},
{
'name': 'Empty Subcategory',
'parent_id': cls.filled_category.id,
'website_id': cls.website.id,
},
{
'name': 'Empty Subcategory of Empty Category',
'parent_id': cls.empty_category.id,
'website_id': cls.website.id,
},
])
# Add one dummy product in one of the subcategories
cls._create_product(public_categ_ids=[cls.filled_category.child_id[0].id])
def test_ecommerce_access_public_user(self):
# By default, everyone has access to ecommerce
self.assertTrue(self.website.with_user(self.public_user).has_ecommerce_access())
self.website.ecommerce_access = 'logged_in'
self.assertFalse(self.website.with_user(self.public_user).has_ecommerce_access())
def test_frontend_ecommerce_access_public_user(self):
"""
Ensures that the '/shop' URL returns a 200 OK status code even if categories are empty when
not logged.
"""
self.quick_ref('website_sale.products_categories').active = True
self.quick_ref('website_sale.option_collapse_products_categories').active = False
response = self.url_open('/shop')
self.assertEqual(response.status_code, 200) # Check that customers can access
def test_ecommerce_access_logged_user(self):
# By default, everyone has access to the ecommerce
self.assertTrue(self.website.has_ecommerce_access())
self.website.ecommerce_access = 'logged_in'
# Check if logged-in users still have access to ecommerce after restricting it
self.assertTrue(self.website.has_ecommerce_access())
def test_frontend_ecommerce_access_portal_user(self):
"""
Ensures that the '/shop' URL returns a 200 OK status code even if categories are empty when
logged as portal user.
"""
self.quick_ref('website_sale.products_categories').active = True
self.quick_ref('website_sale.option_collapse_products_categories').active = False
portal_user = self._create_new_portal_user(website_id=self.website.id)
self.authenticate(portal_user.login, portal_user.login)
response = self.url_open('/shop')
self.assertEqual(response.status_code, 200) # Check that customers can access
def test_ecommerce_menu_visibility_public_user(self):
self.menu = self.env['website.menu'].create({
'name': 'Shop',
'url': '/shop',
'parent_id': self.website.menu_id.id,
'sequence': 0,
'website_id': self.website.id,
})
# Check if by default public user can see shop menu
self.menu.with_user(self.public_user).sudo()._compute_visible() # Needs to be sudoed as
# public user can't access _compute_visible
self.assertTrue(self.menu.is_visible)
self.website.ecommerce_access = 'logged_in'
self.menu.with_user(self.public_user).sudo()._compute_visible()
# Check if menu is hidden for public user when ecommerce is restricted
self.assertFalse(self.menu.is_visible)
def test_ecommerce_access_shop_redirection(self):
self.website.ecommerce_access = 'logged_in'
self.authenticate(None, None)
public_category = self.env['product.public.category'].create({
'name': 'Test Category',
})
public_product_template = self.env['product.template'].create({
'name': 'Test Template',
'public_categ_ids': [public_category.id],
'website_published': True,
})
category_slug = self.env["ir.http"]._slug(public_category)
response = self.url_open(f'/shop/category/{category_slug}/page/1', allow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertURLEqual(response.url, f'/web/login?redirect=/shop/category/{category_slug}/page/1')
product_slug = self.env["ir.http"]._slug(public_product_template)
response = self.url_open(f'/shop/{product_slug}', allow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertURLEqual(response.url, f'/web/login?redirect=/shop/{product_slug}')

View file

@ -1,19 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from uuid import uuid4
from werkzeug import urls
from unittest.mock import Mock, patch
from odoo import Command
from odoo.http import root
from odoo.tests import tagged
from odoo.tests import HttpCase, tagged
from odoo.tools import urls
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.website_sale.controllers.main import WebsiteSale as WebsiteSaleController
from odoo.addons.payment import utils as payment_utils
from odoo.addons.website_sale.controllers.delivery import Delivery as WebsiteSaleDeliveryController
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale.controllers.cart import Cart
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('at_install')
class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
@tagged('post_install', '-at_install')
class TestWebsiteSaleExpressCheckoutFlows(WebsiteSaleCommon, HttpCase):
""" The goal of this method class is to test the address management on
express checkout.
"""
@ -21,20 +22,9 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env.ref('website.default_website')
cls.country_id = cls.env.ref('base.be').id
cls.sale_order = cls.env['sale.order'].create({
'partner_id': cls.website.user_id.partner_id.id,
'website_id': cls.website.id,
'order_line': [Command.create({
'product_id': cls.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'website_published': True,
'sale_ok': True}).id,
'name': 'Product A',
})]
})
cls.country_id = cls.country_be.id
cls.sale_order = cls.cart
cls.sale_order.partner_id = cls.public_partner.id
cls.express_checkout_billing_values = {
'name': 'Express Checkout Partner',
'email': 'express@check.out',
@ -43,17 +33,68 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
'street2': 'ppp',
'city': 'ooo',
'zip': '1200',
'country': 'US',
'state': 'WA',
'country': "US", # United States
'state': "CA", # California
}
cls.express_checkout_shipping_values = {
'name': "Express Checkout Shipping Partner",
'email': "express_shipping@check.out",
'phone': "1111111111",
'street': "ooo shipping",
'street2': "ppp shipping",
'city': "ooo shipping",
'zip': "25781",
'country': "US", # United States
'state': "WA", # Washington
}
cls.express_checkout_anonymized_shipping_values = {
'city': "ooo shipping",
'zip': "6155",
'country': "AU", # Australia
'state': "WA", # Western Australia
}
cls.express_checkout_anonymized_shipping_values_2 = {
'city': "ooo shipping 2",
'zip': "11519",
'country': "ES", # Spain
'state': "CA", # Cádiz
}
cls.user_demo = cls._create_new_internal_user(**cls.dummy_partner_address_values)
cls.express_checkout_demo_shipping_values = {
'name': cls.user_demo.partner_id.name,
'email': cls.user_demo.partner_id.email,
'phone': cls.user_demo.partner_id.phone,
'street': cls.user_demo.partner_id.street,
'street2': cls.user_demo.partner_id.street2,
'city': cls.user_demo.partner_id.city,
'zip': cls.user_demo.partner_id.zip,
'country': cls.user_demo.partner_id.country_id.code,
'state': cls.user_demo.partner_id.state_id.code,
}
cls.express_checkout_anonymized_demo_shipping_values = {
'city': cls.user_demo.partner_id.city,
'zip': cls.user_demo.partner_id.zip,
'country': cls.user_demo.partner_id.country_id.code,
'state': cls.user_demo.partner_id.state_id.code,
}
cls.express_checkout_demo_shipping_values_2 = {
'name': "Express Checkout Shipping Partner",
'email': "express_shipping@check.out",
'phone': "1111111111",
'street': "ooo shipping",
'street2': "ppp shipping",
'city': cls.user_demo.partner_id.city,
'zip': cls.user_demo.partner_id.zip,
'country': cls.user_demo.partner_id.country_id.code,
'state': cls.user_demo.partner_id.state_id.code,
}
cls.rate_shipment_result = {
'success': True,
'price': 5.0,
'warning_message': "",
}
# Ensure demo user address exists and is valid
cls.user_demo.write({
'street': "215 Vine St",
'city': "Scranton",
'zip': "18503",
'country_id': cls.env.ref('base.us').id,
'state_id': cls.env.ref('base.state_us_39').id,
})
def assertPartnerShippingValues(self, partner, shipping_values):
for key, expected in shipping_values.items():
@ -69,31 +110,17 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
"Partner's state should be within partner's country",
)
def _make_json_rpc_request(self, url, data=None):
""" Make a JSON-RPC request to the provided URL.
:param str url: The URL to make the request to
:param dict data: The data to be send in the request body in JSON-RPC 2.0 format
:return dict: The result of the JSON-RPC request
"""
rpc_request = {
"jsonrpc": "2.0",
"method": "call",
"id": str(uuid4()),
"params": data,
}
result = self.url_open(
url,
data=json.dumps(rpc_request).encode(),
headers={"Content-Type": "application/json"},
timeout=None,
def test_express_checkout_takes_order_amount_without_delivery(self):
"""Test that the amount to pay does not include the delivery costs in express checkout."""
amount_without_delivery = payment_utils.to_minor_currency_units(
self.cart.amount_total, self.cart.currency_id
)
self.carrier.fixed_price = 20
self.cart.set_delivery_line(self.carrier, self.carrier.fixed_price)
with MockRequest(self.env, sale_order_id=self.cart.id, website=self.website):
payment_values = Cart()._get_express_shop_payment_values(self.cart)
if not result.ok:
return {}
return result.json().get("result", {})
self.assertEqual(payment_values['minor_amount'], amount_without_delivery)
def test_express_checkout_public_user(self):
""" Test that when using express checkout as a public user, a new partner is created. """
@ -101,10 +128,10 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(), WebsiteSale._express_checkout_route
), params={
'billing_address': dict(self.express_checkout_billing_values)
}
)
@ -126,10 +153,10 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(), WebsiteSale._express_checkout_route
), params={
'billing_address': {
'name': self.user_demo.partner_id.name,
'email': self.user_demo.partner_id.email,
@ -174,10 +201,10 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(), WebsiteSale._express_checkout_route
), params={
'billing_address': dict(self.express_checkout_billing_values)
}
)
@ -195,10 +222,10 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(), WebsiteSale._express_checkout_route
), params={
'billing_address': dict(self.express_checkout_billing_values)
}
)
@ -210,3 +237,299 @@ class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
new_partner,
self.express_checkout_billing_values,
)
def test_express_checkout_public_user_shipping_address_change(self):
""" Test that when using express checkout as a public user and selecting a shipping address,
a new partner is created if the partner of the SO is the public partner.
"""
session = self.authenticate(None, None)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values,
),
},
)
new_partner = self.sale_order.partner_shipping_id
self.assertNotEqual(new_partner, self.website.user_id.partner_id)
self.assertTrue(new_partner.name.endswith(self.sale_order.name))
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_anonymized_shipping_values,
)
def test_express_checkout_public_user_shipping_address_change_twice(self):
""" Test that when using express checkout as a public user and selecting a shipping address
more than once, a new partner is created if the partner of the SO is the public partner
(only creates one new partner that is updated).
"""
session = self.authenticate(None, None)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values,
),
},
)
new_partner = self.sale_order.partner_shipping_id
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values_2,
),
},
)
self.assertEqual(new_partner.id, self.sale_order.partner_shipping_id.id)
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_anonymized_shipping_values_2,
)
def test_express_checkout_registered_user_exisiting_shipping_address_change(self):
""" Test that when using express checkout as a registered user and selecting an exisiting
shipping address, the existing partner (the one of the SO) is reused.
"""
self.sale_order.partner_id = self.user_demo.partner_id.id
session = self.authenticate(self.user_demo.login, self.user_demo.login)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values,
),
},
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
def test_express_checkout_registered_user_new_shipping_address_change(self):
""" Test that when using express checkout as a registered user and selecting a new shipping
address, a new partner is created if the partner of the SO or his children are different
from the delivery information received.
"""
self.sale_order.partner_id = self.user_demo.partner_id.id
session = self.authenticate(self.user_demo.login, self.user_demo.login)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values,
),
},
)
new_partner = self.sale_order.partner_shipping_id
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
self.assertNotEqual(new_partner.id, self.user_demo.partner_id.id)
self.assertTrue(new_partner.name.endswith(self.sale_order.name))
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_anonymized_shipping_values,
)
def test_express_checkout_registered_user_new_shipping_address_change_twice(self):
""" Test that when using express checkout as a registered user and selecting a new
shipping address more than once, a new partner is created if the partner of the SO is
the public partner (only creates one new partner that is updated).
"""
self.sale_order.partner_id = self.user_demo.partner_id.id
session = self.authenticate(self.user_demo.login, self.user_demo.login)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values,
),
},
)
new_partner = self.sale_order.partner_shipping_id
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_shipping_values_2,
),
},
)
self.assertEqual(new_partner.id, self.sale_order.partner_shipping_id.id)
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_anonymized_shipping_values_2
)
def test_express_checkout_partial_delivery_address_context_key(self):
""" Test that when using express checkout with only partial delivery information,
`express_checkout_partial_delivery_address` context key is in the context.
"""
delivery_carrier_mock = Mock()
delivery_carrier_mock.rate_shipment = Mock(
# Since we didn't mock the product ids for the mocked carrier, return an unsuccessful
# response to skip the part where the product ids are checked on the carrier
return_value=dict(self.rate_shipment_result, success=False)
)
WebsiteSaleDeliveryController._get_rate(
delivery_carrier_mock, self.sale_order, is_express_checkout_flow=True
)
sale_order = delivery_carrier_mock.rate_shipment.call_args[0][0]
self.assertTrue(sale_order.env.context.get('express_checkout_partial_delivery_address'))
def test_express_checkout_registered_user_with_shipping_option(self):
""" Test that when you use the express checkout as a registered user and the shipping
address sent by the express checkout form exactly matches one of the addresses linked
to this user in Odoo, we do not create a new partner and reuse the existing one.
"""
self.sale_order.partner_id = self.user_demo.partner_id.id
session = self.authenticate(self.user_demo.login, self.user_demo.login)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
shipping_options = self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_demo_shipping_values,
),
},
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_route,
),
params={
'billing_address': dict(self.express_checkout_billing_values),
'shipping_address': dict(self.express_checkout_demo_shipping_values),
'shipping_option': shipping_options['delivery_methods'][0],
},
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
def test_express_checkout_registered_user_with_shipping_option_new_address(self):
""" Test that when you use the express checkout as a registered user and the shipping
address sent by the express checkout form doesn't exist in Odoo, we create a new
partner.
"""
self.sale_order.partner_id = self.user_demo.partner_id.id
session = self.authenticate(self.user_demo.login, self.user_demo.login)
session['sale_order_id'] = self.sale_order.id
root.session_store.save(session)
with patch(
'odoo.addons.delivery.models.delivery_carrier.DeliveryCarrier.rate_shipment',
return_value=self.rate_shipment_result
):
# Won't create a new partner because the partial information are the same as an
# exisiting partner linked to the SO
shipping_options = self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_delivery_route,
),
params={
'partial_delivery_address': dict(
self.express_checkout_anonymized_demo_shipping_values,
),
},
)
self.assertEqual(self.sale_order.partner_shipping_id, self.user_demo.partner_id)
# Will create a new partner because the complete shipping information differs from
# the partner actually selected
self.make_jsonrpc_request(
urls.urljoin(
self.base_url(),
WebsiteSaleDeliveryController._express_checkout_route
),
params={
'billing_address': dict(self.express_checkout_billing_values),
'shipping_address': dict(self.express_checkout_demo_shipping_values_2),
'shipping_option': shipping_options['delivery_methods'][0],
},
)
self.assertNotEqual(
self.sale_order.partner_shipping_id.id, self.user_demo.partner_id.id
)
self.assertFalse(
self.sale_order.partner_shipping_id.name.endswith(self.sale_order.name)
)
def test_express_checkout_compute_taxes_returns_amount_without_delivery(self):
"""
Test that the /express_checkout/compute_taxes route returns the minor amount
without delivery costs.
"""
websiteSaleDeliveryController = WebsiteSaleDeliveryController()
delivery_product = self.env['product.product'].create({'name': 'Delivery'})
delivery = self.env['delivery.carrier'].create({
'name': 'Normal Delivery Charges',
'fixed_price': 10,
'delivery_type': 'fixed',
'product_id': delivery_product.id,
})
self.sale_order._create_delivery_line(delivery, 10)
expected_amount = self.sale_order._compute_amount_total_without_delivery()
with MockRequest(self.env, website=self.website, sale_order_id=self.sale_order.id):
minor_amount = websiteSaleDeliveryController.express_checkout_shipping_address_compute_taxes()
self.assertEqual(minor_amount, expected_amount * 100)

View file

@ -0,0 +1,41 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
@tagged('-at_install', 'post_install')
class TestFuzzy(ProductVariantsCommon):
def test_variant_default_code(self):
website = self.env.ref('website.default_website')
line = self.product_template_sofa.attribute_line_ids
value_red = line.product_template_value_ids[0]
value_blue = line.product_template_value_ids[1]
value_green = line.product_template_value_ids[2]
product_red = self.product_template_sofa._get_variant_for_combination(value_red)
product_blue = self.product_template_sofa._get_variant_for_combination(value_blue)
product_green = self.product_template_sofa._get_variant_for_combination(value_green)
product_red.default_code = 'RED_12345'
product_blue.default_code = 'BLUE_ABCDE'
product_green.default_code = 'GREEN_98765'
self.cr.flush()
options = {
'displayDescription': True, 'displayDetail': True, 'display_currency': True,
'displayExtraDetail': True, 'displayExtraLink': True,
'displayImage': True, 'allowFuzzy': True
}
results_count, _, fuzzy_term = website._search_with_fuzzy('products_only', 'RED234', 5, 'name asc', options)
self.assertEqual(1, results_count, "Should have found red")
self.assertEqual('red_12345', fuzzy_term, "Should suggest red")
results_count, _, fuzzy_term = website._search_with_fuzzy('products_only', 'GROEN98765', 5, 'name asc', options)
self.assertEqual(1, results_count, "Should have found green")
self.assertEqual('green_98765', fuzzy_term, "Should suggest green")
results_count, _, fuzzy_term = website._search_with_fuzzy('products_only', 'BLUABCE', 5, 'name asc', options)
self.assertEqual(1, results_count, "Should have found blue")
self.assertEqual('blue_abcde', fuzzy_term, "Should suggest blue")
results_count, _, fuzzy_term = website._search_with_fuzzy('products_only', 'SQWBRNZ', 5, 'name asc', options)
self.assertEqual(0, results_count, "Should have found none")
self.assertIsNone(fuzzy_term, "Should have no suggestion")

View file

@ -0,0 +1,54 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.tests import tagged
from odoo.addons.payment.models.payment_provider import PaymentProvider
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.sale.tests.common import SaleCommon
@tagged('post_install', '-at_install')
class TestPaymentProviderVisibility(PaymentHttpCommon, SaleCommon):
def test_payment_provider_visibility_with_portal(self):
"""Check providers availability on the sales portal.
The current website must be considered to filter the providers.
"""
website_portal = self.env['website'].get_current_website()
website_shop = self.env['website'].create({'name': "Shop Website"})
base_url = self.env['ir.config_parameter'].sudo().get_base_url()
website_portal.write({'domain': base_url})
self.provider.write({'website_id': website_portal.id})
restricted_provider = self.env['payment.provider'].sudo().search([('name', '=', 'Demo'), ('company_id', '=', website_shop.company_id.id)])
restricted_provider.write({'state': 'test', 'website_id': website_shop.id})
url_so = self.sale_order.get_portal_url()
self.sale_order.require_payment = True
portal_url = f"{website_portal.domain}{url_so}"
with patch(
'odoo.addons.website_payment.models.payment_provider.PaymentProvider._get_compatible_providers',
side_effect=lambda *args, **kwargs: PaymentProvider._get_compatible_providers(
self.env['payment.provider'], *args, **kwargs
),
) as mock_method:
self.url_open(portal_url, allow_redirects=True)
mock_method.assert_called_once()
self.assertEqual(mock_method.call_args.kwargs['website_id'], website_portal.id)
mock_method.call_args.kwargs.pop('show_non_tokenize_provider', None)
providers = self.env['payment.provider']._get_compatible_providers(
*mock_method.call_args.args, **mock_method.call_args.kwargs
)
self.assertIn(self.provider.id, providers.ids, "The visible provider should be visible.")
self.assertNotIn(
restricted_provider.id, providers.ids, "The restricted provider shouldn't be visible."
)

View file

@ -0,0 +1,66 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestProductPublicCategory(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
def create_multi(vals_list):
return list(map(Command.create, vals_list))
cls.published_product, cls.unpublished_product = cls.env['product.template'].create([
{'name': 'Published Product', 'is_published': True},
{'name': 'Unpublished Product', 'is_published': False},
])
cls.env['product.public.category'].search([]).unlink()
cls.env['product.public.category'].create([
{
'name': '1',
'child_id': create_multi([
{
'name': '1.1',
'child_id': create_multi([{'name': '1.1.1'}]),
},
{'name': '1.2', 'product_tmpl_ids': [Command.link(cls.published_product.id)]},
]),
},
{
'name': '2',
'child_id': create_multi([
{
'name': '2.1',
'child_id': create_multi([{
'name': '2.1.1',
'product_tmpl_ids': [
Command.link(cls.published_product.id),
Command.link(cls.unpublished_product.id)
],
}]),
},
{'name': '2.2'},
]),
},
{'name': '3', 'product_tmpl_ids': [Command.link(cls.unpublished_product.id)]},
])
def test_search_has_published_products(self):
published_categs = set(self.env['product.public.category'].search(
[('has_published_products', 'not in', (False,))]
).mapped('name'))
self.assertSetEqual(published_categs, {'1', '1.2', '2', '2.1', '2.1.1'})
def test_search_does_not_have_published_products(self):
unpublished_categs = set(self.env['product.public.category'].search(
[('has_published_products', '!=', True)]
).mapped('name'))
self.assertSetEqual(unpublished_categs, {'1.1', '1.1.1', '2.2', '3'})

View file

@ -0,0 +1,59 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import tagged
from odoo.addons.sale.tests.common import SaleCommon
@tagged('post_install', '-at_install')
class TestSaleOrder(SaleCommon):
def test_delivery_methods_match_order_company(self):
company_1 = self.env['res.company'].create({'name': 'Test Company 1'})
company_2 = self.env['res.company'].create({'name': 'Test Company 2'})
product_delivery_1 = self.env['product.product'].create(
{
'name': 'Delivery Product 1',
'type': 'service',
'company_id': company_1.id,
}
)
product_delivery_2 = self.env['product.product'].create(
{
'name': 'Delivery Product 2',
'type': 'service',
'company_id': company_2.id,
}
)
delivery_1 = self.env['delivery.carrier'].create(
{
'name': 'Delivery 1',
'delivery_type': 'fixed',
'product_id': product_delivery_1.id,
'is_published': True,
}
)
delivery_2 = self.env['delivery.carrier'].create(
{
'name': 'Delivery 2',
'delivery_type': 'fixed',
'product_id': product_delivery_2.id,
'is_published': True,
}
)
sale_order = self.env['sale.order'].create(
{
'partner_id': self.partner.id,
'company_id': company_1.id,
'order_line': [
Command.create(
{
'product_id': self.product.id,
}
)],
}
)
available_dms = sale_order._get_delivery_methods()
self.assertIn(delivery_1, available_dms)
self.assertNotIn(delivery_2, available_dms)

View file

@ -1,113 +1,107 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import lxml.html
import odoo.tests
from odoo.fields import Command
from odoo.tests import tagged
from odoo import api, Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, TransactionCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website.tools import MockRequest
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
_logger = logging.getLogger(__name__)
@odoo.tests.tagged('post_install', '-at_install')
class TestUi(HttpCaseWithUserDemo):
@tagged('post_install', '-at_install')
class TestSaleProcess(HttpCaseWithUserDemo, WebsiteSaleCommon, HttpCaseWithWebsiteUser):
def setUp(self):
super(TestUi, self).setUp()
product_product_7 = self.env['product.product'].create({
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.storage_box = cls.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({
cls.product_attribute_legs = cls.env['product.attribute'].create({
'name': 'Legs',
'sequence': 10,
'value_ids': [
Command.create({
'name': 'Steel',
'sequence': 1,
}),
Command.create({
'name': 'Aluminium',
'sequence': 2,
}),
],
})
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({
cls.conference_chair = cls.env['product.template'].create({
'name': 'Conference Chair',
'list_price': 16.50,
'website_published': True,
'sale_ok': True,
'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)],
'accessory_product_ids': [Command.link(cls.storage_box.id)],
'attribute_line_ids': [
Command.create({
'attribute_id': cls.product_attribute_legs.id,
'value_ids': [Command.set(cls.product_attribute_legs.value_ids.ids)],
})
],
})
self.product_product_1_product_template = self.env['product.template'].create({
cls.chair_floor_protection = cls.env['product.template'].create({
'name': 'Chair floor protection',
'list_price': 12.0,
})
# Crappy hack: But otherwise the "Proceed To Checkout" modal button won't be displayed
if 'optional_product_ids' in self.env['product.template']:
self.product_product_11_product_template.optional_product_ids = [(6, 0, self.product_product_1_product_template.ids)]
if 'optional_product_ids' in cls.env['product.template']:
cls.conference_chair.optional_product_ids = [Command.set(cls.chair_floor_protection.ids)]
self.env['account.journal'].create({'name': 'Cash - Test', 'type': 'cash', 'code': 'CASH - Test'})
# 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',
cls.env['account.journal'].create({
'name': 'Cash - Test',
'type': 'cash',
'code': 'CASH - Test',
})
# Avoid Shipping/Billing address page
cls.env.ref('base.partner_admin').write(cls.dummy_partner_address_values)
cls.partner_website_user.write(cls.dummy_partner_address_values)
if cls.env['ir.module.module']._get('payment_custom').state == 'installed':
transfer_provider = cls.env.ref('payment.payment_provider_transfer')
transfer_provider.write({
'state': 'enabled',
'is_published': True,
})
transfer_provider._transfer_ensure_pending_msg_is_set()
def test_01_admin_shop_tour(self):
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'shop', login='admin')
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'test_01_admin_shop_tour', login='admin')
def test_01_cart_update_check(self):
self.start_tour('/', 'shop_update_cart', login='admin')
def test_02_admin_checkout(self):
if self.env['ir.module.module']._get('payment_custom').state != 'installed':
self.skipTest("Transfer provider is not installed")
transfer_provider = self.env.ref('payment.payment_provider_transfer')
transfer_provider.write({
'state': 'enabled',
'is_published': True,
})
transfer_provider._transfer_ensure_pending_msg_is_set()
self.start_tour("/", 'shop_buy_product', login="admin")
def test_03_demo_checkout(self):
self.partner_demo.write(self.dummy_partner_address_values)
if self.env['ir.module.module']._get('payment_custom').state != 'installed':
self.skipTest("Transfer provider is not installed")
transfer_provider = self.env.ref('payment.payment_provider_transfer')
transfer_provider.write({
'state': 'enabled',
'is_published': True,
})
transfer_provider._transfer_ensure_pending_msg_is_set()
self.start_tour("/", 'shop_buy_product', login="demo")
def test_04_admin_website_sale_tour(self):
if self.env['ir.module.module']._get('payment_custom').state != 'installed':
self.skipTest("Transfer provider is not installed")
self.env.ref('payment.payment_provider_transfer').write({
'state': 'enabled',
'is_published': True,
})
self.env.company.country_id = self.env.ref('base.us')
self.env.company.country_id = self.country_us
tax_group = self.env['account.tax.group'].create({'name': 'Tax 15%'})
tax = self.env['account.tax'].create({
'name': 'Tax 15%',
@ -120,7 +114,6 @@ class TestUi(HttpCaseWithUserDemo):
'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',
})
@ -128,12 +121,14 @@ class TestUi(HttpCaseWithUserDemo):
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_1')
self.start_tour(self.env['website'].get_client_action_url('/shop/cart'), 'website_sale_tour_backend', login='admin')
self.start_tour(
self.env['website'].get_client_action_url('/shop/cart'),
'website_sale_tour_backend',
login='admin'
)
self.start_tour("/", 'website_sale_tour_2', login="admin")
def test_05_google_analytics_tracking(self):
@ -155,7 +150,7 @@ class TestUi(HttpCaseWithUserDemo):
'name': 'Colored T-Shirt',
'standard_price': 500,
'list_price': 750,
'detailed_type': 'consu',
'type': 'consu',
'website_published': True,
'attribute_line_ids': [
Command.create({
@ -170,339 +165,59 @@ class TestUi(HttpCaseWithUserDemo):
self.env['product.template'].create({
'name': 'Basic Shirt',
'standard_price': 500,
'detailed_type': 'consu',
'type': 'consu',
'website_published': True
})
self.start_tour("/shop", 'google_analytics_add_to_cart')
def test_06_public_user_shop_repair(self):
""" Public user purchasing repair service products in website shop. """
if self.env['ir.module.module']._get('repair').state != 'installed':
self.skipTest("Repair is not installed")
@odoo.tests.tagged('post_install', '-at_install')
class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUserPortal):
''' 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.partner_demo.company_id = self.env.ref('base.main_company')
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, company_id=None):
values = {
'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,
'website_published': True,
'sale_ok': True}).id,
'name': 'Product A',
})]
}
if company_id:
values['company_id'] = company_id
return self.env['sale.order'].create(values)
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) as req:
req.httprequest.method = "POST"
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
self.portal_user = self.user_portal
self.portal_partner = self.portal_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) as req:
req.httprequest.method = "POST"
# 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/internal user, should not edit name or email address of 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.")
self.assertNotEqual(self.demo_partner.name, self.default_address_values['name'], "Employee cannot change their name during the checkout process.")
self.assertNotEqual(self.demo_partner.email, self.default_address_values['email'], "Employee cannot change their email during the checkout process.")
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) as req:
req.httprequest.method = "POST"
# 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")
def test_04_pl_reset_on_login(self):
"""Check that after login, the SO pricelist is correctly recomputed."""
test_user = self.env['res.users'].create({
'name': 'Toto',
'login': 'long_enough_password',
'password': 'long_enough_password',
})
eur_pl = self.env['product.pricelist'].create({
'name': 'EUR_test',
'website_id': self.website.id,
'code': 'EUR_test',
})
test_user.partner_id.property_product_pricelist = eur_pl
public_user_env = self.env(user=self.website.user_id)
so = self._create_so(public_user_env.user.partner_id.id)
with MockRequest(self.env, website=self.website, sale_order_id=so.id, website_sale_current_pl=so.pricelist_id.id):
order = self.website.sale_get_order()
pl = order.pricelist_id
self.assertNotEqual(pl, eur_pl)
order_b = self.website.with_user(test_user).sale_get_order()
self.assertEqual(order, order_b)
self.assertEqual(order_b.pricelist_id, eur_pl)
# TEST WEBSITE & MULTI COMPANY
def test_05_create_so_with_website_and_multi_company(self):
''' This test ensure that the company_id of the website set on the order
is the same as the env company or the one set on the order.
'''
self._setUp_multicompany_env()
# No company on the SO
so = self._create_so(self.demo_partner.id)
self.assertEqual(so.company_id, self.website.company_id)
# Same company on the SO and the env user company but no website
with self.assertRaises(ValueError, msg="Should not be able to create SO with company different than the website company"):
self._create_so(self.demo_partner.id, self.company_a.id)
# Same company on the SO and the website company
so = self._create_so(self.demo_partner.id, self.company_b.id)
self.assertEqual(so.company_id, self.website.company_id)
# Different company on the SO and the env user company
with self.assertRaises(ValueError, msg="Should not be able to create SO with company different than the website company"):
self._create_so(self.demo_partner.id, self.company_c.id)
def test_06_portal_user_address_and_company(self):
''' Same as test_03 but with portal user '''
self._setUp_multicompany_env()
so = self._create_so(self.portal_partner.id)
self.env['sale.order'].create({
'partner_id': self.partner_portal.id,
'state': 'sent',
})
env = api.Environment(self.env.cr, self.portal_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) as req:
req.httprequest.method = "POST"
# 1. Portal user, new shipping, same with the log in user
self.WebsiteSaleController.address(**self.default_address_values)
new_shipping = self._get_last_address(self.portal_partner)
self.assertTrue(new_shipping.company_id != self.env.user.company_id, "Portal 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. Portal user, edit billing
self.default_address_values['partner_id'] = self.portal_partner.id
self.WebsiteSaleController.address(**self.default_address_values)
# Name cannot be changed if there are issued invoices
self.assertNotEqual(self.portal_partner.name, self.default_address_values['name'], "Portal User should not be able to change the name if they have invoices under their name.")
def test_07_change_fiscal_position(self):
"""
Check that the sale order is updated when you change fiscal position.
Change fiscal position by modifying address during checkout process.
"""
self.env.company.country_id = self.env.ref('base.us')
partner = self.env['res.partner'].create({'name': 'test'})
be_address_POST, nl_address_POST = [
{
'name': 'Test name', 'email': 'test@email.com', 'street': 'test',
'city': 'test', 'zip': '3000', 'country_id': self.env.ref('base.be').id, 'submitted': 1,
'partner_id': partner.id,
'callback': '/shop/checkout',
},
{
'name': 'Test name', 'email': 'test@email.com', 'street': 'test',
'city': 'test', 'zip': '3000', 'country_id': self.env.ref('base.nl').id, 'submitted': 1,
'partner_id': partner.id,
'callback': '/shop/checkout',
},
]
tax_10_incl, tax_20_excl, tax_15_incl = self.env['account.tax'].create([
{'name': 'Tax 10% incl', 'amount': 10, 'price_include': True},
{'name': 'Tax 20% excl', 'amount': 20, 'price_include': False},
{'name': 'Tax 15% incl', 'amount': 15, 'price_include': True},
])
self.env['account.fiscal.position'].create([
{
'sequence': 1,
'name': 'BE',
'auto_apply': True,
'country_id': self.env.ref('base.be').id,
'tax_ids': [Command.create({'tax_src_id': tax_10_incl.id, 'tax_dest_id': tax_20_excl.id})],
},
{
'sequence': 2,
'name': 'NL',
'auto_apply': True,
'country_id': self.env.ref('base.nl').id,
'tax_ids': [Command.create({'tax_src_id': tax_10_incl.id, 'tax_dest_id': tax_15_incl.id})],
},
])
product = self.env['product.product'].create({
'name': 'Product test',
'list_price': 100,
'website_published': True,
self.env['product.template'].create({
'name': 'Test Repair Service',
'type': 'service',
'service_tracking': 'repair',
'sale_ok': True,
'taxes_id': [tax_10_incl.id]
'is_published': True,
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'website_id': self.website.id,
'order_line': [Command.create({
'product_id': product.id,
'name': 'Product test',
})]
self.start_tour("/", 'shop_repair_product', login=None)
def test_checkout_with_rewrite(self):
# check that checkout page can be open with step rewritten
self.env['website.rewrite'].create({
'name': 'Test Address Rename',
'redirect_type': '308',
'url_from': '/shop/address',
'url_to': '/test/address',
})
self.env['website.rewrite'].create({
'name': 'Test Checkout Rename',
'redirect_type': '308',
'url_from': '/shop/checkout',
'url_to': '/test/checkout',
})
self._create_so(partner_id=self.user_demo.partner_id.id)
self.authenticate('demo', 'demo')
response = self.url_open('/shop/address')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.url[-13:], '/test/address')
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 9.09, 100.0]
)
# check that navigation (next and previous checkout steps) are correct
allowed_steps_domain = self.website._get_allowed_steps_domain()
checkout_step = self.env.ref('website_sale.checkout_step_delivery')
previous_step = checkout_step._get_previous_checkout_step(allowed_steps_domain)
next_step = checkout_step._get_next_checkout_step(allowed_steps_domain)
root = lxml.html.fromstring(response.content)
self.assertEqual(len(root.xpath(f'//a[@href="{previous_step.step_href}"]//span[text()="{previous_step.back_button_label}"]')), 2)
self.assertEqual(len(root.xpath(f'//a[@name="website_sale_main_button"][not(@href)]//span[text()="{next_step.main_button_label}"]')), 2)
env = api.Environment(self.env.cr, self.website.user_id.id, {})
with MockRequest(self.env, website=self.website.with_env(env), sale_order_id=so.id) as req:
req.httprequest.method = "POST"
self.WebsiteSaleController.address(**be_address_POST)
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 18.18, 109.09] # (100 : (1 + 10%)) * (1 + 20%) = 109.09
)
self.WebsiteSaleController.address(**nl_address_POST)
self.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 13.64, 104.55] # (100 : (1 + 10%)) * (1 + 15%) = 104.55
)
def test_08_payment_term_when_address_change(self):
''' This test ensures that the payment term set when triggering
`onchange_partner_id` by changing the address of a website sale
order is computed by `sale_get_payment_term`.
'''
self._setUp_multicompany_env()
product_id = self.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'website_published': True,
'sale_ok': True}).id
env = api.Environment(self.env.cr, self.portal_user.id, {})
with MockRequest(env, website=self.website.with_env(env).with_context(website_id=self.website.id)) as req:
req.httprequest.method = "POST"
self.WebsiteSaleController.cart_update(product_id)
so = self.portal_user.sale_order_ids[0]
self.assertTrue(so.payment_term_id, "A payment term should be set by default on the sale order")
self.default_address_values['partner_id'] = self.portal_partner.id
self.default_address_values['name'] = self.portal_partner.name
self.WebsiteSaleController.address(**self.default_address_values)
self.assertTrue(so.payment_term_id, "A payment term should still be set on the sale order")
so.website_id = False
self.WebsiteSaleController.address(**self.default_address_values)
self.assertFalse(so.payment_term_id, "The website default payment term should not be set on a sale order not coming from the website")
def test_update_same_address_billing_shipping_edit(self):
''' Phone field should be required when updating an adress for billing and shipping '''
self.env['product.product'].create({
'name': 'Office Chair Black TEST',
'list_price': 12.50,
'is_published': True,
})
self.start_tour("/shop", 'update_billing_shipping_address', login="website_user")

View file

@ -1,5 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
@ -15,11 +16,31 @@ class TestSitemap(HttpCase):
'name': 'Level 1',
}, {
'name': 'Level 2',
}, {
'name': 'Level 2A',
}])
self.cats[3].parent_id = self.cats[1].id
self.cats[2].parent_id = self.cats[1].id
self.cats[1].parent_id = self.cats[0].id
# 'Level 2' cetegory must have at least one published product to be visible by public users
self.env['product.product'].create({
'name': 'Dummy product',
'list_price': 100.0,
'public_categ_ids': [Command.link(self.cats[2].id)],
'is_published': True,
})
# 'Level 2A' category will contains only archived products, so should be hidden to public users
prodA = self.env['product.product'].create({
'name': 'Dummy product A',
'list_price': 100.0,
'public_categ_ids': [Command.link(self.cats[3].id)],
'is_published': True,
})
prodA.product_tmpl_id.active = False
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.")
self.assertIn(level2_url, resp.text, "Category entry in sitemap should be prefixed by its parent hierarchy.")
level2A_url = '/shop/category/level-0-level-1-level-2a-%s' % self.cats[3].id
self.assertNotIn(level2A_url, resp.text, "Category entry with no active products should not be listed in sitemap.")

View file

@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import Command
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website.tools import MockRequest
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.http_routing.tests.common import MockRequest
from odoo.addons.website_sale.controllers.main import WebsiteSale
_logger = logging.getLogger(__name__)
ATTACHMENT_DATA = [
@ -45,8 +47,9 @@ class TestProductPictureController(HttpCase):
def _create_product_images(self):
with MockRequest(self.product.env, website=self.website):
self.WebsiteSaleController.add_product_images(
self.WebsiteSaleController.add_product_media(
[{'id': attachment.id} for attachment in self.attachments],
'image',
self.product.id,
self.product.product_tmpl_id.id,
)
@ -114,8 +117,9 @@ class TestProductPictureController(HttpCase):
})
self.assertEqual(0, len(product_template.product_variant_ids))
with MockRequest(product_template.env, website=self.website):
self.WebsiteSaleController.add_product_images(
self.WebsiteSaleController.add_product_media(
[{'id': self.attachments[0].id}],
'image',
False,
product_template.id,
[product_template_attribute_line.product_template_value_ids[0].id],
@ -223,26 +227,21 @@ class TestProductPictureController(HttpCase):
@tagged('post_install', '-at_install')
class TestWebsiteSaleEditor(HttpCase):
class TestWebsiteSaleEditor(HttpCaseWithWebsiteUser):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['res.users'].create({
'name': 'Restricted Editor',
'login': 'restricted',
'password': 'restricted',
'groups_id': [Command.set([
cls.env.ref('base.group_user').id,
cls.env.ref('sales_team.group_sale_manager').id,
cls.env.ref('website.group_website_restricted_editor').id
])]
})
cls.user_website_user.group_ids += cls.env.ref('sales_team.group_sale_manager')
cls.user_website_user.group_ids += cls.env.ref('product.group_product_manager')
def test_category_page_and_products_snippet(self):
category = self.env['product.public.category'].create({
'name': 'Test Category',
})
self.env['product.public.category'].create({
'parent_id': category.id,
'name': 'Test Category - Child',
})
self.env['product.template'].create({
'name': 'Test Product',
'website_published': True,
@ -254,7 +253,7 @@ class TestWebsiteSaleEditor(HttpCase):
'name': 'Test Product Outside Category',
'website_published': True,
})
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'category_page_and_products_snippet_edition', login='restricted')
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'category_page_and_products_snippet_edition', login="admin")
self.start_tour('/shop', 'category_page_and_products_snippet_use', login=None)
def test_website_sale_restricted_editor_ui(self):
@ -263,4 +262,57 @@ class TestWebsiteSaleEditor(HttpCase):
'website_sequence': 0,
'website_published': True,
})
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'website_sale_restricted_editor_ui', login='restricted')
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'website_sale_restricted_editor_ui', login="website_user")
@tagged('post_install', '-at_install')
class TestProductVideoUpload(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].browse(1)
cls.WebsiteSaleController = WebsiteSale()
cls.product = cls.env['product.product'].create({
'name': 'Test Video Product',
'standard_price': 100.0,
'list_price': 120.0,
'website_published': True,
})
cls.video_data = {
'src': 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', # A placeholder video URL
'name': 'Test Video',
}
def _upload_video(self):
with MockRequest(self.product.env, website=self.website):
self.WebsiteSaleController.add_product_media(
[{'src': self.video_data['src'], 'name': self.video_data['name']}],
'video',
self.product.id,
self.product.product_tmpl_id.id,
)
def test_video_upload(self):
# Upload a video to the product
self._upload_video()
# Retrieve the product's media data
video_url = self.product.product_template_image_ids[0].video_url
image_1920 = self.product.product_template_image_ids[0].image_1920
# Check that the video URL and thumbnail are correctly saved
self.assertEqual(video_url, self.video_data['src'])
self.assertIsNotNone(image_1920) # Ensure a thumbnail was generated
# Verify that the video was added as part of the media
self.assertEqual(len(self.product.product_template_image_ids), 1)
def test_video_upload_invalid(self):
# Try to upload invalid video data (e.g., empty src)
with MockRequest(self.product.env, website=self.website):
with self.assertRaises(ValidationError):
self.WebsiteSaleController.add_product_media(
[{'src': '', 'name': 'Invalid Video'}],
'video',
self.product.id,
self.product.product_tmpl_id.id,
)

View file

@ -77,7 +77,8 @@ class TestAddToCartSnippet(HttpCase):
'street2': "",
'city': "Ramillies",
'zip': 1367,
'country_id': self.env.ref('base.be').id
'country_id': self.env.ref('base.be').id,
'phone': "+32 123456789"
})
self.env.ref('base.user_admin').country_id = self.env.ref('base.be')
self.start_tour("/", 'add_to_cart_snippet_tour', login="admin")

View file

@ -1,70 +1,91 @@
# coding: utf-8
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from functools import partial
from unittest.mock import patch
from odoo.addons.base.tests.common import TransactionCaseWithUserPortal
from odoo.addons.website_sale.controllers.main import WebsiteSale, PaymentPortal
from odoo.addons.website.tools import MockRequest
from odoo.addons.website_sale.models.product_template import ProductTemplate
from odoo.exceptions import UserError
from odoo.tests.common import tagged
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
from odoo.addons.website_sale.controllers.cart import Cart
from odoo.addons.website_sale.controllers.combo_configurator import (
WebsiteSaleComboConfiguratorController,
)
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale.controllers.payment import PaymentPortal
from odoo.addons.website_sale.models.product_template import ProductTemplate
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class WebsiteSaleCart(TransactionCaseWithUserPortal):
class TestWebsiteSaleCart(ProductVariantsCommon, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super(WebsiteSaleCart, cls).setUpClass()
cls.website = cls.env['website'].browse(1)
super().setUpClass()
cls.user_portal = cls._create_new_portal_user()
cls.WebsiteSaleController = WebsiteSale()
cls.WebsiteSaleCartController = Cart()
cls.public_user = cls.env.ref('base.public_user')
def test_add_cart_deleted_product(self):
# Create a published product then unlink it
product = self.env['product.product'].create({
cls.product = cls.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
'website_published': True,
'lst_price': 1000.0,
'standard_price': 800.0,
})
product_id = product.id
product.unlink()
with self.assertRaises(UserError):
with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)):
self.WebsiteSaleController.cart_update_json(product_id=product_id, add_qty=1)
def test_add_cart_deleted_product(self):
# Unlink published product.
product_template_id = self.product.product_tmpl_id
product_id = self.product.id
self.product.unlink()
website = self.website.with_user(self.public_user)
with self.assertRaises(UserError), MockRequest(website.env, website=website):
self.WebsiteSaleCartController.add_to_cart(
product_template_id=product_template_id,
product_id=product_id,
quantity=1,
)
def test_add_cart_unpublished_product(self):
# Try to add an unpublished product
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
})
self.product.website_published = False
# Environment must be public user as admin can add unpublished products to cart
website = self.website.with_user(self.public_user)
with self.assertRaises(UserError):
with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
with self.assertRaises(UserError), MockRequest(website.env, website=website):
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
# public but remove sale_ok
product.sale_ok = False
product.website_published = True
self.product.sale_ok = False
self.product.website_published = True
with self.assertRaises(UserError):
with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
with self.assertRaises(UserError), MockRequest(website.env, website=website):
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
def test_add_cart_archived_product(self):
# Try to add an archived product
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
})
product.active = False
self.product.active = False
website = self.website.with_user(self.public_user)
with self.assertRaises(UserError):
with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
with self.assertRaises(UserError), MockRequest(website.env, website=website):
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
def test_zero_price_product_rule(self):
"""
@ -80,75 +101,106 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
})
product_consu = self.env['product.product'].create({
'name': 'Cannot be zero price',
'detailed_type': 'consu',
'type': 'consu',
'list_price': 0,
'website_published': True,
})
product_service = self.env['product.product'].create({
'name': 'Can be zero price',
'detailed_type': 'service',
'type': 'service',
'list_price': 0,
'website_published': True,
})
with patch.object(ProductTemplate, '_get_product_types_allow_zero_price', lambda pt: ['service']):
with self.assertRaises(UserError, msg="'consu' product type is not allowed to have a 0 price sale"), \
MockRequest(self.env, website=website_prevent_zero_price):
self.WebsiteSaleController.cart_update_json(product_id=product_consu.id, add_qty=1)
with (
self.assertRaises(UserError, msg="'consu' product type is not allowed to have a 0 price sale"),
MockRequest(self.env, website=website_prevent_zero_price)
):
self.WebsiteSaleCartController.add_to_cart(
product_template_id=product_consu.product_tmpl_id,
product_id=product_consu.id,
quantity=1,
)
# service types should not raise a UserError
with (
patch.object(ProductTemplate, '_get_product_types_allow_zero_price', lambda pt: ['no']),
MockRequest(self.env, website=website_prevent_zero_price),
):
# service_tracking 'no' should not raise error
with MockRequest(self.env, website=website_prevent_zero_price):
self.WebsiteSaleController.cart_update_json(product_id=product_service.id, add_qty=1)
self.WebsiteSaleCartController.add_to_cart(
product_template_id=product_service.product_tmpl_id,
product_id=product_service.id,
quantity=1,
)
def test_update_cart_before_payment(self):
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
'website_published': True,
'lst_price': 1000.0,
'standard_price': 800.0,
})
website = self.website.with_user(self.public_user)
with MockRequest(product.with_user(self.public_user).env, website=website):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
sale_order = website.sale_get_order()
with MockRequest(website.env, website=website) as request:
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
sale_order = request.cart
sale_order.access_token = 'test_token'
old_amount = sale_order.amount_total
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
# Try processing payment with the old amount
with self.assertRaises(UserError):
PaymentPortal().shop_payment_transaction(sale_order.id, sale_order.access_token, amount=old_amount)
PaymentPortal().shop_payment_transaction(
sale_order.id,
sale_order.access_token,
amount=old_amount
)
def test_check_order_delivery_before_payment(self):
website = self.website.with_user(self.public_user)
with MockRequest(website.env, website=website):
sale_order = self.env['sale.order'].create({
'partner_id': self.public_user.id,
'order_line': [Command.create({'product_id': self.product.id})],
'access_token': 'test_token',
})
# Try processing payment with a storable product and no carrier_id
with self.assertRaises(ValidationError):
PaymentPortal().shop_payment_transaction(sale_order.id, sale_order.access_token)
def test_update_cart_zero_qty(self):
# Try to remove a product that has already been removed
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
'website_published': True,
'lst_price': 1000.0,
'standard_price': 800.0,
})
portal_user = self.user_portal
website = self.website.with_user(portal_user)
SaleOrderLine = self.env['sale.order.line']
with MockRequest(product.with_user(portal_user).env, website=website):
with MockRequest(website.env, website=website) as request:
# add the product to the cart
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
sale_order = website.sale_get_order()
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
sale_order = request.cart
self.assertEqual(sale_order.amount_untaxed, 1000.0)
# remove the product from the cart
self.WebsiteSaleController.cart_update_json(product_id=product.id, line_id=sale_order.order_line.id, set_qty=0)
self.WebsiteSaleCartController.update_cart(
line_id=sale_order.order_line.id,
quantity=0,
)
self.assertEqual(sale_order.amount_total, 0.0)
self.assertEqual(sale_order.order_line, SaleOrderLine)
# removing the product again doesn't add a line with zero quantity
self.WebsiteSaleController.cart_update_json(product_id=product.id, set_qty=0)
self.assertEqual(sale_order.order_line, SaleOrderLine)
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=0)
self.WebsiteSaleCartController.update_cart(
line_id=sale_order.order_line.id,
quantity=0,
)
self.assertEqual(sale_order.cart_quantity, 0.0)
self.assertEqual(sale_order.order_line, SaleOrderLine)
def test_unpublished_accessory_product_visibility(self):
@ -158,18 +210,244 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
'is_published': False,
})
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': True,
'website_published': True,
'accessory_product_ids': [Command.link(accessory_product.id)]
self.product.accessory_product_ids = [Command.link(accessory_product.id)]
self.empty_cart._cart_add(product_id=self.product.id)
self.assertEqual(len(self.empty_cart.with_user(self.public_user)._cart_accessories()), 0)
def test_cart_new_fpos_from_geoip(self):
fpos_be = self.env["account.fiscal.position"].create({
'name': 'Fiscal Position BE',
'country_id': self.country_be.id,
'company_id': self.company.id,
'auto_apply': True,
})
website = self.website.with_user(self.public_user)
with MockRequest(product.with_user(self.public_user).env, website=self.website.with_user(self.public_user)):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
sale_order = website.sale_get_order()
self.assertEqual(len(sale_order._cart_accessories()), 0)
with MockRequest(website.env, website=website, country_code='BE') as request:
self.assertEqual(request.fiscal_position, fpos_be)
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
self.assertEqual(
request.cart.fiscal_position_id, fpos_be,
"Fiscal position should be determined from GEOIP country for public users."
)
def test_cart_update_with_fpos(self):
# We will test that the mapping of an 10% included tax by a 6% by a fiscal position is taken
# into account when updating the cart
self._enable_pricelists()
pricelist = self.pricelist
# Create fiscal position mapping taxes 10% -> 6%
fpos = self.env['account.fiscal.position'].create({
'name': 'test',
})
# Add 10% tax on product
tax10, tax6 = self.env['account.tax'].create([
{
'name': "Test tax 10",
'amount': 10,
'price_include_override': 'tax_included',
'amount_type': 'percent'
}, {
'name': "Test tax 6",
'fiscal_position_ids': fpos,
'amount': 6,
'price_include_override': 'tax_included',
'amount_type': 'percent'
},
])
tax6.original_tax_ids = tax10
test_product = self.env['product.product'].create({
'name': 'Test Product',
'list_price': 110,
'taxes_id': [Command.set([tax10.id])],
})
# Add discount of 50% for pricelist
pricelist.write({
'item_ids': [
Command.create({
'base': "list_price",
'compute_price': "percentage",
'percent_price': 50,
}),
],
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [
Command.create({
'product_id': test_product.id,
})
]
})
sol = so.order_line
self.assertEqual(round(sol.price_total), 55.0, "110$ with 50% discount 10% included tax")
self.assertEqual(round(sol.price_tax), 5.0, "110$ with 50% discount 10% included tax")
so.fiscal_position_id = fpos
so._recompute_taxes()
so._cart_update_line_quantity(line_id=sol.id, quantity=2)
self.assertEqual(
round(sol.price_total),
106,
"2 units @ 100$ with 50% discount + 6% tax (mapped from fp 10% -> 6%)"
)
def test_cart_update_with_fpos_no_variant_product(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 for no_variant product
# Add 10% tax on product
fpos = self.env['account.fiscal.position'].create({
'name': 'test',
})
tax10, tax0 = self.env['account.tax'].create([
{
'name': "Test tax 10",
'amount': 10,
'price_include_override': 'tax_included',
'amount_type': 'percent'
}, {
'name': "Test tax 0",
'fiscal_position_ids': fpos,
'amount': 0,
'price_include_override': 'tax_included',
'amount_type': 'percent'
},
])
tax0.original_tax_ids = tax10
# create an attribute with one variant
product_attribute = self.env['product.attribute'].create({
'name': 'test_attr',
'display_type': 'radio',
'create_variant': 'no_variant',
'value_ids': [
Command.create({
'name': 'pa_value',
'sequence': 1,
}),
],
})
product_template = self.env['product.template'].create({
'name': 'prod_no_variant',
'list_price': 110,
'taxes_id': [Command.set([tax10.id])],
'is_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)],
}),
],
})
product = product_template.product_variant_id
# create a so for user using the fiscal position
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [
Command.create({
'product_id': product.id,
})
]
})
sol = so.order_line
self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax")
so.fiscal_position_id = fpos
so._recompute_taxes()
so._cart_update_line_quantity(line_id=sol.id, quantity=2)
self.assertEqual(
round(sol.price_total),
200,
"200$ with public price+ 0% tax (mapped from fp 10% -> 0%)"
)
def test_cart_lines_aggregation(self):
# Adding a product with the same no_variant attributes combination twice should create only
# one SOLine
product_no_variants = self.env['product.template'].create({
'name': 'No variants product (TEST)',
'attribute_line_ids': [Command.create({
'attribute_id': self.no_variant_attribute.id,
'value_ids': [Command.set(self.no_variant_attribute.value_ids.ids)],
})],
})
no_variant_ptavs = product_no_variants.attribute_line_ids.product_template_value_ids
no_variant_ptav = no_variant_ptavs[0]
add_one = partial(
self.empty_cart._cart_add,
product_id=product_no_variants.product_variant_id.id,
quantity=1,
)
self.assertEqual(len(self.empty_cart.order_line), 0)
add_one(no_variant_attribute_value_ids=no_variant_ptav.ids)
self.assertEqual(len(self.empty_cart.order_line), 1)
add_one(no_variant_attribute_value_ids=no_variant_ptav.ids)
self.assertEqual(len(self.empty_cart.order_line), 1)
self.assertEqual(self.empty_cart.order_line.product_uom_qty, 2)
# Providing `no_variant_attribute_value_ids` should be optional if there's only 1 value...
product_no_variants.attribute_line_ids.value_ids = self.no_variant_attribute.value_ids[0]
add_one(no_variant_attribute_value_ids=[])
self.assertEqual(len(self.empty_cart.order_line), 1)
self.assertEqual(self.empty_cart.order_line.product_uom_qty, 3)
# ...except if it's a multi-checkbox attribute, making the value optional
self.no_variant_attribute.display_type = 'multi'
add_one(no_variant_attribute_value_ids=[])
self.assertEqual(len(self.empty_cart.order_line), 2)
self.assertEqual(self.empty_cart.order_line.mapped('product_uom_qty'), [3, 1])
add_one(no_variant_attribute_value_ids=no_variant_ptav.ids)
self.assertEqual(len(self.empty_cart.order_line), 2)
self.assertEqual(self.empty_cart.order_line.mapped('product_uom_qty'), [4, 1])
def test_cart_new_pricelist_from_geoip(self):
"""Check that, when adding a new partner to a website order, the partner's GeoIP
is factored into the pricelist recomputation.
"""
self._enable_pricelists()
eu_group = self.env.ref('base.europe')
not_eu_group = self.env['res.country.group'].create({
'name': "Not EU",
'country_ids': self.env['res.country'].search([
('id', 'not in', eu_group.country_ids.ids),
]).ids,
})
_pricelist_eu, pricelist_not_eu = self.env['product.pricelist'].create([{
'name': "EU",
'country_group_ids': eu_group.ids,
'website_id': self.website.id,
'sequence': 1,
}, {
'name': "Not EU",
'country_group_ids': not_eu_group.ids,
'website_id': self.website.id,
'sequence': 2,
}])
website = self.website.with_user(self.public_user)
with MockRequest(website.env, website=website, country_code='US') as request:
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
cart = request.cart
self.assertEqual(cart.pricelist_id, pricelist_not_eu)
cart.partner_id = self.partner.create({'name': "New Partner"})
self.assertEqual(cart.pricelist_id, pricelist_not_eu)
def test_remove_archived_product_line(self):
"""If an order has a line containing an archived product,
@ -182,9 +460,13 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
'sale_ok': True,
'website_published': True,
})
with MockRequest(self.env(user=user), website=website):
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=1)
order = website.sale_get_order()
with MockRequest(self.env(user=user), website=website) as request:
self.WebsiteSaleCartController.add_to_cart(
product_template_id=product.product_tmpl_id,
product_id=product.id,
quantity=1,
)
order = request.cart
# pre-condition: the order contains an active product
self.assertRecordValues(order.order_line, [{
@ -194,7 +476,7 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
# Act: archive the product and open the cart
product.active = False
self.WebsiteSaleController.cart()
self.WebsiteSaleCartController.cart()
# Assert: the line has been removed
self.assertFalse(order.order_line)
@ -205,8 +487,8 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
# Arrange
user = self.public_user
website = self.website.with_user(user)
with MockRequest(self.env(user=user), website=website):
order = website.sale_get_order(force_create=True)
with MockRequest(self.env(user=user), website=website) as request:
order = request.website._create_cart()
order.order_line = [
Command.create({
"name": "Note",
@ -220,9 +502,80 @@ class WebsiteSaleCart(TransactionCaseWithUserPortal):
}])
# Act: open the cart
self.WebsiteSaleController.cart()
self.WebsiteSaleCartController.cart()
# Assert: the line is still there
self.assertRecordValues(order.order_line, [{
"display_type": "line_note",
}])
def test_checkout_no_delivery_method_available(self):
portal_user = self.user_portal
website = self.website.with_user(portal_user)
portal_user.write(self.dummy_partner_address_values)
self.carrier.country_ids = [Command.set((2,))]
self.product.type = 'consu'
with (
MockRequest(website.env, website=website) as request,
patch(
'odoo.addons.website_sale.models.sale_order.SaleOrder._get_preferred_delivery_method',
return_value=self.env['delivery.carrier'],
)
):
order = request.website._create_cart()
order.order_line = [
Command.create({
'product_id': self.product.id,
'product_uom_qty': 1.0,
})
]
self.WebsiteSaleController.shop_checkout()
def test_add_to_cart_company_branch(self):
"""Test that a product/website from a company branch
can be added to the cart."""
branch_a = self.env["res.company"].create(
{
"name": "Branch A",
"parent_id": self.env.company.id,
}
)
website = self.env["website"].create(
{
"name": "Branch A Website",
"company_id": branch_a.id,
}
)
self.product.company_id = branch_a
with MockRequest(
self.product.with_user(website.user_id).env,
website=website.with_user(website.user_id),
):
branch_a.invalidate_recordset()
data = WebsiteSaleComboConfiguratorController().website_sale_combo_configurator_get_data(
date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
product_tmpl_id=self.product.product_tmpl_id.id,
quantity=1,
)
self.assertEqual(data["quantity"], 1)
def test_get_cart_after_company_change(self):
"""Finding the customer cart shouldn't crash even if their company changed."""
internal_user = self.env.ref('base.user_admin')
website = self.website.with_user(internal_user)
with MockRequest(website.env, website=website):
# Create a cart for the user
self.WebsiteSaleCartController.add_to_cart(
product_template_id=self.product.product_tmpl_id,
product_id=self.product.id,
quantity=1,
)
# Change the user's company (will also update the user's partner)
other_company = self.env['res.company'].create({'name': "Other Company"})
internal_user.company_ids = [Command.link(other_company.id)]
internal_user.company_id = other_company
with MockRequest(website.env, website=website) as request:
# We shouldn't find any abandonned cart if the customer isn't allowed to
# buy from this website (because their contact belongs to another company)
self.assertFalse(request.cart)

View file

@ -1,15 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
from dateutil.relativedelta import relativedelta
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.base.tests.common import TransactionCaseWithUserPortal
from odoo.addons.mail.models.mail_template import MailTemplate
class TestWebsiteSaleCartAbandonedCommon(HttpCaseWithUserPortal):
class TestWebsiteSaleCartAbandonedCommon(TransactionCaseWithUserPortal):
def send_mail_patched(self, sale_order_id):
email_got_sent = False
def check_send_mail_called(this, res_id, email_values, *args, **kwargs):
nonlocal email_got_sent
if res_id == sale_order_id:
email_got_sent = True
with patch.object(MailTemplate, 'send_mail', check_send_mail_called):
self.env['website']._send_abandoned_cart_email()
return email_got_sent
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartAbandoned(TestWebsiteSaleCartAbandonedCommon):
@classmethod
def setUpClass(cls):
@ -48,6 +65,7 @@ class TestWebsiteSaleCartAbandonedCommon(HttpCaseWithUserPortal):
'product_id': product.id,
'product_uom_qty': 1,
}]]
cls.payment_method_id = cls.env.ref('payment.payment_method_unknown').id
cls.so0before = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website0.id,
@ -107,20 +125,6 @@ class TestWebsiteSaleCartAbandonedCommon(HttpCaseWithUserPortal):
'order_line': add_order_line,
})
def send_mail_patched(self, sale_order_id):
email_got_sent = False
def check_send_mail_called(this, res_id, email_values, *args, **kwargs):
nonlocal email_got_sent
if res_id == sale_order_id:
email_got_sent = True
with patch.object(MailTemplate, 'send_mail', check_send_mail_called):
self.env['website']._send_abandoned_cart_email()
return email_got_sent
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartAbandoned(TestWebsiteSaleCartAbandonedCommon):
def test_search_abandoned_cart(self):
"""Make sure the search for abandoned carts uses the delay and public partner specified in each website."""
SaleOrder = self.env['sale.order']
@ -149,6 +153,15 @@ class TestWebsiteSaleCartAbandoned(TestWebsiteSaleCartAbandonedCommon):
website = self.env['website'].get_current_website()
website.send_abandoned_cart_email = True
website.write(
{
"send_abandoned_cart_email_activation_time": (
datetime.utcnow()
- relativedelta(hours=website.cart_abandoned_delay)
)
- relativedelta(minutes=10)
}
)
product = self.env['product.product'].create({
'name': 'The Product'
@ -225,6 +238,7 @@ class TestWebsiteSaleCartAbandoned(TestWebsiteSaleCartAbandonedCommon):
})
transaction = self.env['payment.transaction'].create({
'provider_id': 15,
'payment_method_id': self.payment_method_id,
'partner_id': self.customer.id,
'reference': abandoned_sale_order.name,
'amount': abandoned_sale_order.amount_total,

View file

@ -0,0 +1,53 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartNotification(HttpCase, ProductVariantsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env.company.website_id
cls.size_attribute.create_variant = 'no_variant'
cls.product_tmpl_1 = cls.env['product.template'].create({
'name': 'website_sale_cart_notification_product_1',
'type': 'consu',
'website_published': True,
'list_price': 1000,
})
cls.product_tmpl_2 = cls.env['product.template'].create({
'name': 'website_sale_cart_notification_product_2',
'type': 'consu',
'website_published': True,
'list_price': 5000,
'attribute_line_ids': [Command.create({
'attribute_id': cls.size_attribute.id,
'value_ids': [Command.set([
cls.size_attribute_s.id,
cls.size_attribute_m.id,
cls.size_attribute_l.id,
])],
})],
})
def test_website_sale_cart_notification_tax_included(self):
self.env.ref('website_sale.product_search').active = True
self.website.show_line_subtotals_tax_selection = 'tax_included'
self.start_tour("/", 'website_sale_cart_notification_tax_included')
def test_website_sale_cart_notification_tax_excluded(self):
self.env.ref('website_sale.product_search').active = True
self.website.show_line_subtotals_tax_selection = 'tax_excluded'
self.start_tour("/", 'website_sale_cart_notification_tax_excluded')
def test_website_sale_cart_notification_qty_and_total(self):
""" Check that adding product into cart which is already in the cart only display newly
added qty count and total."""
self.env.ref('website_sale.product_search').active = True
self.start_tour("/", 'website_sale_cart_notification_qty_and_total')

View file

@ -1,23 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.models import Command
from odoo.tests.common import tagged
from unittest.mock import patch
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.website.tools import MockRequest
from odoo.fields import Command
from odoo.tests.common import JsonRpcException, tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class WebsiteSaleCartPayment(PaymentCommon):
class WebsiteSaleCartPayment(PaymentHttpCommon, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].get_current_website()
with MockRequest(cls.env, website=cls.website):
cls.order = cls.website.sale_get_order(force_create=True) # Create the cart to retrieve
cls.tx = cls.env['payment.transaction'].create({
'payment_method_id': cls.payment_method_id,
'amount': cls.amount,
'currency_id': cls.currency.id,
'provider_id': cls.provider.id,
@ -25,17 +26,17 @@ class WebsiteSaleCartPayment(PaymentCommon):
'operation': 'online_redirect',
'partner_id': cls.partner.id,
})
cls.order.write({'transaction_ids': [Command.set([cls.tx.id])]})
cls.cart.write({'transaction_ids': [Command.set([cls.tx.id])]})
def test_unpaid_orders_can_be_retrieved(self):
""" Test that fetching sales orders linked to a payment transaction in the states 'draft',
'cancel', or 'error' returns the orders. """
for unpaid_order_tx_state in ('draft', 'cancel', 'error'):
self.tx.state = unpaid_order_tx_state
with MockRequest(self.env, website=self.website, sale_order_id=self.order.id):
with MockRequest(self.env, website=self.website, sale_order_id=self.cart.id) as request:
self.assertEqual(
self.website.sale_get_order(),
self.order,
request.cart,
self.cart,
msg=f"The transaction state '{unpaid_order_tx_state}' should not prevent "
f"retrieving the linked order.",
)
@ -43,12 +44,37 @@ class WebsiteSaleCartPayment(PaymentCommon):
def test_paid_orders_cannot_be_retrieved(self):
""" Test that fetching sales orders linked to a payment transaction in the states 'pending',
'authorized', or 'done' returns an empty recordset to prevent updating the paid orders. """
self.tx.provider_id.support_manual_capture = True
self.tx.provider_id.support_manual_capture = 'full_only'
for paid_order_tx_state in ('pending', 'authorized', 'done'):
self.tx.state = paid_order_tx_state
with MockRequest(self.env, website=self.website, sale_order_id=self.order.id):
with MockRequest(self.env, website=self.website, sale_order_id=self.cart.id) as request:
self.assertFalse(
self.website.sale_get_order(),
request.cart,
msg=f"The transaction state '{paid_order_tx_state}' should prevent retrieving "
f"the linked order.",
)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
url = self._build_url(f'/shop/payment/transaction/{self.cart.id}')
route_kwargs = {
'access_token': self.cart._portal_ensure_token(),
'partner_id': self.partner.id, # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self.make_jsonrpc_request(url, route_kwargs)
def test_payment_confirmation_mail(self):
"""Check that a salesperson gets assigned when sending payment confirmation mails."""
salesperson = self.env.ref('base.user_admin')
self.website.salesperson_id = salesperson
self.cart.user_id = False
self.tx._set_pending()
with patch.object(self.env.registry['sale.order'], '_send_order_notification_mail') as mock:
self.tx._post_process()
self.assertEqual(mock.call_count, 1, "One payment confirmation mail should be sent")
self.assertEqual(
self.cart.user_id,
salesperson,
"Salesperson should get assigned when sending payment confirmation mail",
)

View file

@ -1,20 +0,0 @@
# -*- 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
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartPopover(HttpCase):
def setUp(self):
super(TestWebsiteSaleCartPopover, self).setUp()
self.env['product.product'].create({
'name': 'website_sale_cart_popover_tour_product',
'type': 'consu',
'website_published': True,
'list_price': 1000,
})
def test_website_sale_cart_popover(self):
self.start_tour("/", 'website_sale_cart_popover_tour', login="admin")

View file

@ -1,15 +1,18 @@
# -*- 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.tests.common import 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.ref('base.user_admin').write({
'email': 'mitchell.admin@example.com',
})
self.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'list_price': 2950.0,
@ -22,50 +25,49 @@ class TestWebsiteSaleCartRecovery(HttpCaseWithUserPortal):
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartRecoveryServer(TransactionCase):
def setUp(self):
res = super(TestWebsiteSaleCartRecoveryServer, self).setUp()
@classmethod
def setUpClass(cls):
super().setUpClass()
self.customer = self.env['res.partner'].create({
cls.customer = cls.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()
cls.recovery_template_default = cls.env.ref('website_sale.mail_template_sale_cart_recovery')
cls.recovery_template_custom1 = cls.recovery_template_default.copy()
cls.recovery_template_custom2 = cls.recovery_template_default.copy()
self.website0 = self.env['website'].create({
cls.website0 = cls.env['website'].create({
'name': 'web0',
'cart_recovery_mail_template_id': self.recovery_template_default.id,
'cart_recovery_mail_template_id': cls.recovery_template_default.id,
})
self.website1 = self.env['website'].create({
cls.website1 = cls.env['website'].create({
'name': 'web1',
'cart_recovery_mail_template_id': self.recovery_template_custom1.id,
'cart_recovery_mail_template_id': cls.recovery_template_custom1.id,
})
self.website2 = self.env['website'].create({
cls.website2 = cls.env['website'].create({
'name': 'web2',
'cart_recovery_mail_template_id': self.recovery_template_custom2.id,
'cart_recovery_mail_template_id': cls.recovery_template_custom2.id,
})
self.so0 = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': self.website0.id,
cls.so0 = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.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,
cls.so1 = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.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,
cls.so2 = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.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(
@ -112,7 +114,7 @@ class TestWebsiteSaleCartRecoveryServer(TransactionCase):
sent_mail = {}
for order in orders:
mail = self.env["mail.mail"].search([
('record_name', '=', order['name'])
('res_id', '=', order['id'])
])
sent_mail.update({order: mail})
@ -121,6 +123,6 @@ class TestWebsiteSaleCartRecoveryServer(TransactionCase):
"Each cart recovery mail has been sent exactly once."
)
self.assertTrue(
all(order.access_token in sent_mail[order].body for order in orders),
all(order.access_token in sent_mail[order].body_html for order in orders),
"Each mail should contain the access token of the corresponding SO."
)

View file

@ -0,0 +1,67 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleCheckoutSteps(WebsiteSaleCommon):
def test_get_existing_specific_extra_step(self):
specific_extra_step = self.website._get_checkout_step('/shop/extra_info')
generic_extra_step = self.env.ref('website_sale.checkout_step_extra')
self.assertNotEqual(specific_extra_step, generic_extra_step)
self.assertEqual(specific_extra_step.website_id, self.website)
def test_translate_checkout_steps(self):
"""Verify that loading languages correctly translates website-specific steps."""
CheckoutStep = self.env['website.checkout.step']
IrModuleModule = self.env['ir.module.module']
if self.env['website'].search_count([]) == 1:
self.env['website'].create({'name': "My Website 2"})
default_payment_step = self.env.ref('website_sale.checkout_step_payment')
default_payment_step_FR = default_payment_step.with_context(lang='fr_FR')
website_1_step_FR, website_2_step_FR = CheckoutStep.with_context(lang='fr_FR').search([
('website_id', '!=', False),
('step_href', '=', default_payment_step.step_href),
], limit=2)
# Activate French
if not (lang_fr := self.env.ref('base.lang_fr')).active:
lang_fr.active = True
for fname, field in CheckoutStep._fields.items():
if field.translate and default_payment_step[fname]:
default_payment_step_FR[fname] = f"{default_payment_step[fname]} (FR)"
# Have a different translation for a specific website
website_1_step_FR.name = "Pay in French"
# Load translations without overwrite
IrModuleModule._load_module_terms(['website_sale'], ['fr_FR'])
CheckoutStep.invalidate_model(['name'])
self.assertEqual(
website_1_step_FR.name, "Pay in French",
"Loading translations without overwrite should keep existing translation",
)
self.assertIn(
website_2_step_FR.name,
("Payment (FR)", "Paiement"), # "Paiement" in case translation was already loaded
"Loading translations should add missing term from default step",
)
# Load translations with overwrite
IrModuleModule._load_module_terms(['website_sale'], ['fr_FR'], overwrite=True)
CheckoutStep.invalidate_model(['name'])
self.assertEqual(
website_1_step_FR.name, "Payment (FR)",
"Loading translations with overwrite should update existing term from template",
)
# Ensure all translatable fields were updated
for fname, field in CheckoutStep._fields.items():
if field.translate:
self.assertEqual(website_1_step_FR[fname], default_payment_step_FR[fname])
self.assertEqual(website_2_step_FR[fname], default_payment_step_FR[fname])

View file

@ -0,0 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleComboConfigurator(HttpCase, WebsiteSaleCommon):
def test_website_sale_combo_configurator(self):
no_variant_attribute = self.env['product.attribute'].create({
'name': "No variant attribute",
'create_variant': 'no_variant',
'value_ids': [
Command.create({'name': "A"}),
Command.create({'name': "B", 'is_custom': True, 'default_extra_price': 1}),
],
})
product_a1 = self.env['product.template'].create({
'name': "Product A1",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': no_variant_attribute.id,
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
}),
],
})
combo_a = self.env['product.combo'].create({
'name': "Combo A",
'combo_item_ids': [
Command.create({'product_id': product_a1.product_variant_id.id, 'extra_price': 5}),
Command.create({'product_id': self._create_product(name="Product A2").id}),
],
})
combo_b = self.env['product.combo'].create({
'name': "Combo B",
'combo_item_ids': [
Command.create({'product_id': self._create_product(name="Product B1").id}),
Command.create({'product_id': self._create_product(name="Product B2").id}),
],
})
self._create_product(
name="Combo product",
list_price=25,
type='combo',
combo_ids=[
Command.link(combo_a.id),
Command.link(combo_b.id),
],
)
self.website.show_line_subtotals_tax_selection = 'tax_included'
self.start_tour('/', 'website_sale_combo_configurator')
def test_website_sale_combo_configurator_single_configuration(self):
""" Test that the combo configurator isn't shown if there's a single configuration. """
no_variant_attribute = self.env['product.attribute'].create({
'name': "No variant attribute",
'create_variant': 'no_variant',
'value_ids': [Command.create({'name': "A"})],
})
product = self.env['product.template'].create({
'name': "Test product",
'attribute_line_ids': [
Command.create({
'attribute_id': no_variant_attribute.id,
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
}),
],
'website_published': True,
})
combo = self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [Command.create({'product_id': product.product_variant_id.id})],
})
self._create_product(
name="Combo product",
type='combo',
combo_ids=[Command.link(combo.id)],
)
self.start_tour('/', 'website_sale_combo_configurator_single_configuration')
def test_website_sale_combo_configurator_single_configurable_item(self):
""" Test that the combo configurator is shown if there's a single combo item, but that combo
item is configurable.
"""
no_variant_attribute = self.env['product.attribute'].create({
'name': "No variant attribute",
'create_variant': 'no_variant',
'value_ids': [Command.create({'name': "A", 'is_custom': True})],
})
product = self.env['product.template'].create({
'name': "Test product",
'attribute_line_ids': [
Command.create({
'attribute_id': no_variant_attribute.id,
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
}),
],
})
combo = self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [Command.create({'product_id': product.product_variant_id.id})],
})
self._create_product(
name="Combo product",
type='combo',
combo_ids=[Command.link(combo.id)],
)
self.start_tour('/', 'website_sale_combo_configurator_single_configurable_item')

View file

@ -1,120 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.product.tests.common import ProductCommon
from odoo.tests import tagged
from odoo import Command
@tagged('post_install', '-at_install')
class TestWebsiteSaleFiscalPosition(ProductCommon, HttpCaseWithUserPortal):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.company.country_id = cls.env.ref('base.us')
cls._use_currency('USD')
cls.website = cls.env.ref('website.default_website')
cls.website.company_id = cls.env.company
# Create a fiscal position with a mapping of taxes
cls.tax_15_excl = cls.env['account.tax'].create({
'name': "15% excl",
'type_tax_use': 'sale',
'amount_type': 'percent',
'amount': 15,
'price_include': False,
'include_base_amount': False,
})
cls.tax_0 = cls.env['account.tax'].create({
'name': "0%",
'type_tax_use': 'sale',
'amount_type': 'percent',
'amount': 0,
})
belgium = cls.env.ref('base.be')
cls.fpos_be = cls.env['account.fiscal.position'].create({
'name': "Fiscal Position BE",
'auto_apply': True,
'country_id': belgium.id,
'tax_ids': [Command.create({
'tax_src_id': cls.tax_15_excl.id,
'tax_dest_id': cls.tax_0.id,
})],
})
cls.partner_portal.country_id = belgium
def test_shop_fiscal_position_products_template(self):
"""
The `website_sale.products` template is computationally intensive
and therefore uses the cache.
The goal of this test is to check that this template
is up to date with the fiscal position detected.
"""
# Set setting to display tax included on the website
config = self.env['res.config.settings'].create({})
config.show_line_subtotals_tax_selection = "tax_included"
config.execute()
# Create a pricelist which will be automatically detected
self.env['product.pricelist'].create({
'name': 'EUROPE EUR',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [Command.link(self.env.ref('base.europe').id)],
'sequence': 1,
'currency_id': self.env.ref('base.EUR').id,
})
# Create the product to be used for analysis
self.env["product.product"].create({
'name': "Super product",
'list_price': 40.00,
'taxes_id': self.tax_15_excl.ids,
'website_published': True,
})
# Create a conversion rate (1 USD <=> 2 EUR)
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'company_id': self.env.company.id,
'currency_id': self.env.ref('base.EUR').id,
'company_rate': 2,
'name': '2023-01-01',
})
self.partner_portal.country_id = self.env.ref('base.be')
# [1] By going to the shop page with the portal user,
# a t-cache key `pricelist,products` + `fiscal_position_id` is generated
self.start_tour("/shop", 'website_sale_fiscal_position_portal_tour', login="portal")
# [2] If we return to the page with a public user
# and take the portal user's pricelist,
# the prices must not be those previously calculated for the portal user.
# Because the fiscal position differs from that of the public user.
self.start_tour("/shop", 'website_sale_fiscal_position_public_tour', login="")
def test_recompute_taxes_on_address_change(self):
tax_15_incl = self.tax_15_excl.copy({'name': "15% incl", 'price_include': True})
self.fpos_be.tax_ids.tax_src_id = self.product.taxes_id = tax_15_incl
self.product.website_published = True
cart = self.env['sale.order'].create({
'partner_id': self.partner_portal.id,
'website_id': self.website.id,
'order_line': [Command.create({'product_id': self.product.id})],
})
amount_untaxed = cart.amount_untaxed
self.assertEqual(cart.fiscal_position_id, self.fpos_be)
self.assertEqual(cart.order_line.tax_id, self.tax_0)
self.partner_portal.country_id = self.env.ref('base.us')
self.assertNotEqual(cart.fiscal_position_id, self.fpos_be)
self.assertEqual(cart.order_line.tax_id, tax_15_incl)
self.assertEqual(cart.amount_untaxed, amount_untaxed, "Untaxed amount should not change")
cart.action_confirm()
self.partner_portal.country_id = self.env.ref('base.be')
self.assertEqual(
cart.order_line.tax_id,
tax_15_incl,
"Tax should no longer change after order confirmation",
)

View file

@ -0,0 +1,352 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from lxml import etree
from urllib.parse import urlparse
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.website_sale.tests.common import MockRequest
from odoo.addons.website_sale.tests.common_gmc import WebsiteSaleGMCCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleGMC(WebsiteSaleGMCCommon, HttpCase):
def test_gmc_xml_accessible_if_gmc_setting_enabled(self):
response = self.url_open(self.gmc_feed.url)
self.assertEqual(200, response.status_code)
def test_gmc_xml_not_found_if_gmc_setting_disabled(self):
self.website.enabled_gmc_src = False
response = self.url_open(self.gmc_feed.url)
self.assertEqual(404, response.status_code, 'Not Found')
def test_gmc_xml_correct_xml_format(self):
response = self.url_open(self.gmc_feed.url)
gmc_xml = etree.XML(response.content) # assert valid xml
self.assertEqual(self.website.name, gmc_xml.xpath('//title')[0].text)
self.assertURLEqual('/en', gmc_xml.xpath('//link')[0].text)
self.assertEqual(
'This is the homepage of the website',
gmc_xml.xpath('//description')[0].text,
)
def test_gmc_xml_localization(self):
fr_lang = self.env['res.lang']._activate_lang('fr_FR')
self.website.language_ids += fr_lang
self.gmc_feed.lang_id = fr_lang
self.red_sofa.with_context(lang=fr_lang.code).name = 'Canapé'
self.color_attribute_red.with_context(lang=fr_lang.code).name = 'rouge'
self.update_items()
self.assertRegex(
self.parse_http_location(self.red_sofa_item['link']).path,
f'^\\/{fr_lang.url_code}.*',
'The links must redirect to the french website.',
)
self.assertEqual(
'Canapé (rouge)',
self.red_sofa_item['title'],
)
def test_gmc_xml_pricelist(self):
self.gmc_feed.pricelist_id = self.eur_pricelist
with MockRequest(
self.env,
website=self.website,
website_sale_current_pl=self.pricelist.id,
):
gmc_xml = etree.XML(self.gmc_feed._render_gmc_feed().encode())
self.assertEqual(
'1100.0 EUR', # 1000.0 * 1.1 (EUR rate)
gmc_xml.xpath(
'//item[g:id="SOFA-R"]/g:price', namespaces={'g': 'http://base.google.com/ns/1.0'}
)[0].text
)
def test_gmc_items_required_fields(self):
self.update_items()
for item in self.items.values():
self.assertLessEqual(
{
'id',
'title',
'description',
'link',
'image_link',
'availability',
'price',
'identifier_exists',
},
item.keys(),
) # subseteq
def test_gmc_items_use_internal_reference_if_exists(self):
"""Test prefer internal code to database id"""
# setup: red_sofa.code = 'SOFA-R', blue_sofa.code = False
self.update_items()
self.assertEqual(self.red_sofa.code, self.red_sofa_item['id'])
self.assertEqual(self.blue_sofa.id, self.blue_sofa_item['id'])
def test_gmc_items_link_redirects_to_correct_product_case_default_pricelist(self):
self.update_items()
for product in self.red_sofa + self.blue_sofa:
response = self.url_open(self.items[product]['link'])
self.assertEqual(200, response.status_code)
self.assertURLEqual(product.website_url, response.url)
def test_gmc_items_link_redirects_to_correct_product_case_specific_pricelist(self):
self.gmc_feed.pricelist_id = self.eur_pricelist
self.update_items()
for product in self.red_sofa + self.blue_sofa:
response = self.url_open(self.items[product]['link'])
self.assertEqual(200, response.status_code)
url = urlparse(product.website_url)
self.assertURLEqual(
f'{url.path}'
f'?attribute_values={",".join(str(i) for i in product.product_template_attribute_value_ids.product_attribute_value_id.ids)}'
f'&pricelist={self.eur_pricelist.id}'
f'#{url.fragment}',
response.url,
)
def test_gmc_items_prices_match_website_prices_default(self):
self.update_items()
self.assertEqual('1000.0 USD', self.red_sofa_item['price'])
self.assertEqual('1200.0 USD', self.blue_sofa_item['price'])
self.start_tour(
self.red_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_red_sofa_default',
)
self.start_tour(
self.blue_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_blue_sofa_default',
)
def test_gmc_items_prices_match_website_prices_christmas(self):
self.gmc_feed.pricelist_id = self._create_pricelist(
name="EUR Christmas Sales",
currency_id=self.eur_currency.id,
selectable=True,
item_ids=[
Command.create({
'product_tmpl_id': self.product_template_sofa.id,
'compute_price': 'percentage',
'percent_price': 10.0,
'date_start': datetime.now() - timedelta(1),
'date_end': datetime.now() + timedelta(1),
}),
Command.create({
'compute_price': 'percentage',
'percent_price': 0.0,
}),
],
)
self.update_items()
# 1000.0 (list_price) * 1.1 (EUR rate) - 10% (discount)
self.assertEqual('990.0 EUR', self.red_sofa_item['sale_price'])
# 1200.0 (list_price) * 1.1 (EUR rate) - 10% (discount)
self.assertEqual('1188.0 EUR', self.blue_sofa_item['sale_price'])
# 100.0 (list_price) * 1.1 (EUR rate)
self.assertEqual('110.0 EUR', self.items[self.blanket]['price'])
self.assertNotIn('sale_price', self.items[self.blanket]) # no discount
self.assertNotEqual(self.red_sofa_item['link'], self.blue_sofa_item['link'])
self.start_tour(
self.red_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_red_sofa_christmas',
)
self.start_tour(
self.blue_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_blue_sofa_christmas',
)
def test_gmc_items_prices_match_website_prices_tax_included(self):
# 15% taxes
self.website.show_line_subtotals_tax_selection = 'tax_included'
self.update_items()
self.assertEqual('1150.0 USD', self.red_sofa_item['price'])
self.assertEqual('1380.0 USD', self.blue_sofa_item['price'])
self.start_tour(
self.red_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_red_sofa_tax_included',
)
self.start_tour(
self.blue_sofa_item['link'],
'website_sale_gmc_check_advertised_prices_blue_sofa_tax_included',
)
def test_gmc_items_additional_images_limit_to_10(self):
image = self._create_image('blue')
self.blue_sofa.write({
'product_variant_image_ids': [
Command.create({
'name': f'image {i}',
'image_1920': image,
})
for i in range(12)
]
})
self.update_items()
self.assertEqual(10, len(self.blue_sofa_item['additional_image_link']))
def test_gmc_items_identifier_exists_iff_barcode_exists(self):
self.red_sofa.barcode = '0232344532564'
self.update_items()
self.assertEqual(self.red_sofa.barcode, self.red_sofa_item['gtin'])
self.assertEqual('yes', self.red_sofa_item['identifier_exists'])
self.assertNotIn('gtin', self.blue_sofa_item)
self.assertEqual('no', self.blue_sofa_item['identifier_exists'])
def test_gmc_items_sorted_types(self):
# Furnitures / Sofas
# Furnitures / Indoor Furnitures / Indoor Sofas
furnitures_categ, sofas_categ = self._create_public_category([
{'name': 'Furnitures'},
{'name': 'Sofas', 'sequence': 1},
])
indoor_sofas_categ = self._create_public_category([
{'name': 'Indoor Furnitures', 'parent_id': furnitures_categ.id},
{'name': 'Indoor Sofas', 'sequence': 2},
])
self.public_categories = sofas_categ + indoor_sofas_categ
self.product_template_sofa.public_categ_ids = self.public_categories
self.update_items()
self.assertListEqual(
list(
name.replace('/', '>')
for name in self.public_categories.sorted('sequence').mapped('display_name')
),
self.red_sofa_item['product_type']
)
def test_gmc_items_types_limit_to_5(self):
self.product_template_sofa.write({
'public_categ_ids': [
Command.create({'name': f'Category {i}'}) for i in range(6)
]
})
self.update_items()
self.assertEqual(5, len(self.red_sofa_item['product_type']))
def test_gmc_product_variants(self):
product_one_variant = self.env['product.template'].create({
'name': 'Test product',
'attribute_line_ids': [
Command.create({
'attribute_id': attr.attribute_id.id,
'value_ids': [Command.link(attr.id)],
})
for attr in (self.color_attribute_green + self.size_attribute_l)
],
'list_price': 49.0,
'is_published': True,
}).product_variant_ids
self.products |= product_one_variant
self.update_items()
# same template
self.assertEqual(self.red_sofa_item['item_group_id'], self.blue_sofa_item['item_group_id'])
# no other variant
self.assertNotIn('item_group_id', self.items[product_one_variant])
def test_gmc_items_bundle_if_is_combo_product(self):
self.update_items()
self.assertEqual('yes', self.items[self.sofa_bundle]['is_bundle'])
self.assertEqual('no', self.red_sofa_item['is_bundle'])
def test_gmc_items_sorted_labels(self):
tags = [f'tag {i}' for i in range(3)]
self.product_template_sofa.write({
'product_tag_ids': [
Command.create({'name': tag, 'sequence': i})
for i, tag in enumerate(tags)
]
})
self.update_items()
self.assertListEqual(tags, [name for _, name in self.red_sofa_item['custom_label']])
def test_gmc_items_tags_limit_to_5(self):
self.product_template_sofa.write({
'product_tag_ids': [
Command.create({'name': f"Tag {i}", 'sequence': i})
for i in range(10)
]
})
self.update_items()
self.assertEqual(5, len(self.red_sofa_item['custom_label']))
def test_gmc_items_default_availability_in_stock(self):
self.update_items()
self.assertEqual(
'in_stock',
self.red_sofa_item['availability'],
)
def _setup_6l_water_pack(self):
self.env.user.group_ids |= self.env.ref('uom.group_uom')
uom_litre = self.env.ref('uom.product_uom_pack_6')
base_unit_litre = self.env['website.base.unit'].create({'name': 'L'})
six_pack = self.env['product.product'].create([{
'name': 'Water Pack 6L',
'list_price': 12.0,
'uom_id': uom_litre.id,
'base_unit_count': 6.0,
'base_unit_id': base_unit_litre.id,
'is_published': True,
}])
self.products |= six_pack
return six_pack
def test_gmc_items_unit_pricing_iff_product_reference_enabled(self):
six_pack = self._setup_6l_water_pack()
self.update_items()
self.assertNotIn('unit_pricing_measure', self.items[six_pack])
# enable "Product Reference Price" setting
self.env.user.group_ids |= self.env.ref('website_sale.group_show_uom_price')
self.update_items()
self.assertEqual('6.0l', self.items[six_pack]['unit_pricing_measure'], '$12 / 6l')
def test_gmc_items_dont_send_unsupported_unit(self):
six_pack = self._setup_6l_water_pack()
six_pack.base_unit_id = False # remove `L` alias -> falls back to `Pack of 6`
self.update_items()
self.assertNotIn('unit_pricing_measure', self.items[six_pack])

View file

@ -4,14 +4,22 @@ import base64
import io
from PIL import Image
from odoo.tests.common import HOST
from odoo.tools import config
import odoo.tests
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.website.tests.common import HttpCaseWithWebsiteUser
@odoo.tests.common.tagged('post_install', '-at_install')
class TestWebsiteSaleImage(odoo.tests.HttpCase):
def _create_image(color='black', dims=(1920, 1080), format='JPEG'):
f = io.BytesIO()
Image.new('RGB', dims, color).save(f, format) # type: ignore
f.seek(0)
return base64.b64encode(f.read())
@tagged('post_install', '-at_install')
class TestWebsiteSaleImage(HttpCaseWithWebsiteUser):
# registry_test_mode = False # uncomment to save the product to test in browser
@ -25,81 +33,68 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
color_blue = '#4169E1'
name_blue = 'Royal Blue'
self.env['product.pricelist'].sudo().search([]).action_archive()
# create the color attribute
product_attribute = self.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
'value_ids': [
Command.create({
'name': name_red,
'html_color': color_red,
'sequence': 1,
}),
Command.create({
'name': name_green,
'html_color': color_green,
'sequence': 2,
}),
Command.create({
'name': name_blue,
'html_color': color_blue,
'sequence': 3,
}),
]
})
# 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())
blue_image = _create_image(color=color_blue)
# 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())
red_image = _create_image(color=color_red, dims=(800, 500))
# 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())
green_image = _create_image(color=color_green)
# Template Extra Image 1
f = io.BytesIO()
Image.new('RGB', (124, 147)).save(f, 'GIF')
f.seek(0)
image_gif = base64.b64encode(f.read())
image_gif = _create_image(dims=(124, 147), format='GIF')
# 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())
image_bmp = _create_image(dims=(767, 247), format='BMP')
# 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())
image_png = _create_image(dims=(2147, 3251), format='PNG')
# create the template, without creating the variants
template = self.env['product.template'].with_context(create_product_product=True).create({
template = self.env['product.template'].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})],
'product_template_image_ids': [
Command.create({'name': 'image 1', 'image_1920': image_gif}),
Command.create({'name': 'image 4', 'image_1920': image_svg}),
],
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)],
})
]
})
# 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)]
}])
line = template.attribute_line_ids
value_red = line.product_template_value_ids[0]
value_green = line.product_template_value_ids[1]
@ -205,32 +200,15 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
# self.env.cr.commit() # uncomment to save the product to test in browser
# Make sure we have zoom on click
self.env['ir.ui.view'].with_context(active_test=False).search(
[('key', 'in', ('website_sale.product_picture_magnify_hover', 'website_sale.product_picture_magnify_click', 'website_sale.product_picture_magnify_both'))]
).write({'active': False})
self.env['ir.ui.view'].with_context(active_test=False).search(
[('key', '=', 'website_sale.product_picture_magnify_click')]
).write({'active': True})
# Ensure that only one pricelist is available during the test, with the company currency.
# Ensure that no pricelist is available during the test.
# This ensures that tours with triggers on the amounts will run properly.
# To this purpose, we will ensure that only the public_pricelist is available for the default_website.
public_pricelist = self.env.ref('product.list0')
default_website = self.env.ref('website.default_website')
website_2 = self.env.ref('website.website2', raise_if_not_found=False)
if not website_2:
website_2 = self.env['website'].create({
'name': 'My Website 2',
'domain': '',
'sequence': 20,
})
self.env['product.pricelist'].search([
('id', '!=', public_pricelist.id),
('website_id', 'in', [False, default_website.id])]
).website_id = website_2
public_pricelist.currency_id = self.env.company.currency_id
self.env['product.pricelist'].search([]).action_archive()
self.start_tour("/", 'shop_zoom', login="admin")
self.start_tour("/", 'shop_zoom', login="website_user")
# CASE: unlink move image to fallback if fallback image empty
template.image_1920 = False
@ -286,34 +264,30 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
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())
image = _create_image(color='#FF0000', dims=(800, 500))
# create the color attribute
product_attribute = self.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
'value_ids': [
Command.create({
'name': 'Red',
'sequence': 1,
}),
Command.create({
'name': 'Green',
'sequence': 2,
}),
Command.create({
'name': 'Blue',
'sequence': 3,
}),
]
})
# 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({
template = self.env['product.template'].with_context(create_product_product=False).create({
'name': 'Test subject',
})
@ -324,7 +298,7 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
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_ids': [Command.set(product_attribute.value_ids.ids)]
}])
value_red = line.product_template_value_ids[0]
product_red = template._get_variant_for_combination(value_red)
@ -337,7 +311,7 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
# 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()
product_red.action_archive()
# 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())
@ -347,73 +321,64 @@ class TestWebsiteSaleImage(odoo.tests.HttpCase):
# when there is a template image, the image must be obtained from the template
self.assertEqual(template, template._get_image_holder())
@odoo.tests.common.tagged('post_install', '-at_install')
class TestEnvironmentWebsiteSaleImage(odoo.tests.HttpCase):
def setUp(self):
super(TestEnvironmentWebsiteSaleImage, self).setUp()
# Attachment needed for the replacement of images
IrAttachment = self.env['ir.attachment']
base = "http://%s:%s" % (HOST, config['http_port'])
IrAttachment.create({
'public': True,
'name': 's_default_image.jpg',
'type': 'url',
'url': base + '/web/image/website.s_banner_default_image.jpg',
})
@tagged('post_install', '-at_install')
class TestWebsiteSaleRemoveImage(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# First image (blue) for the template.
color_blue = '#4169E1'
name_blue = 'Royal Blue'
# Red for the variant.
color_red = '#CD5C5C'
name_red = 'Indian Red'
# Green for the replacement
color_green = '#228B22'
# Attachment needed for the replacement of images
cls.env['ir.attachment'].create({
'public': True,
'name': 'green.jpg',
'type': 'binary',
'datas': _create_image(color=color_green)
})
# Create the color attribute.
self.product_attribute = self.env['product.attribute'].create({
cls.product_attribute = cls.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
})
# create the color attribute values
self.attr_values = self.env['product.attribute.value'].create([{
cls.attr_values = cls.env['product.attribute.value'].create([{
'name': name_blue,
'attribute_id': self.product_attribute.id,
'attribute_id': cls.product_attribute.id,
'html_color': color_blue,
'sequence': 1,
}, {
'name': name_red,
'attribute_id': self.product_attribute.id,
'attribute_id': cls.product_attribute.id,
'html_color': color_red,
'sequence': 2,
},
])
f = io.BytesIO()
Image.new('RGB', (1920, 1080), color_blue).save(f, 'JPEG')
f.seek(0)
blue_image = base64.b64encode(f.read())
self.template = self.env['product.template'].with_context(create_product_product=True).create({
cls.template = cls.env['product.template'].with_context(create_product_product=False).create({
'name': 'Test Remove Image',
'image_1920': blue_image,
'image_1920': _create_image(color=color_blue),
})
@odoo.tests.common.tagged('post_install', '-at_install')
class TestRemoveWebsiteSaleImageNoVariant(TestEnvironmentWebsiteSaleImage):
def setUp(self):
super(TestRemoveWebsiteSaleImageNoVariant, self).setUp()
def test_website_sale_add_and_remove_main_product_image_no_variant(self):
self.product = self.env['product.product'].create({
'product_tmpl_id': self.template.id,
})
def test_website_sale_add_and_remove_main_product_image_no_variant(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'add_and_remove_main_product_image_no_variant', login='admin')
self.assertFalse(self.template.image_1920)
self.assertFalse(self.product.image_1920)
@odoo.tests.common.tagged('post_install', '-at_install')
class TestRemoveWebsiteSaleImageVariants(TestEnvironmentWebsiteSaleImage):
def setUp(self):
super(TestRemoveWebsiteSaleImageVariants, self).setUp()
def test_website_sale_remove_main_product_image_with_variant(self):
# Set the color attribute and values on the template.
self.env['product.template.attribute.line'].create([{
'attribute_id': self.product_attribute.id,
@ -423,7 +388,6 @@ class TestRemoveWebsiteSaleImageVariants(TestEnvironmentWebsiteSaleImage):
self.product = self.env['product.product'].create({
'product_tmpl_id': self.template.id,
})
def test_website_sale_remove_main_product_image_with_variant(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'remove_main_product_image_with_variant', login='admin')
self.assertFalse(self.template.image_1920)
self.assertFalse(self.product.image_1920)

View file

@ -29,7 +29,7 @@ class TestWebsiteSaleInvoice(AccountPaymentCommon, SaleCommon):
self.amount = self.sale_order.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._reconcile_after_done()
tx._post_process()
self.assertEqual(self.sale_order.website_id.id, self.website.id)
self.assertEqual(self.sale_order.invoice_ids.website_id.id, self.website.id)

View file

@ -1,22 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
import odoo
from odoo import fields
from odoo import SUPERUSER_ID, fields
from odoo.tests import tagged
from odoo.tests.common import HttpCase
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleMail(HttpCase):
@tagged('post_install', '-at_install', 'mail_thread')
class TestWebsiteSaleMail(HttpCaseWithUserPortal):
def test_01_shop_mail_tour(self):
"""The goal of this test is to make sure sending SO by email works."""
self.env.ref('base.user_admin').write({
'email': 'mitchell.admin@example.com',
})
self.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'list_price': 2950.0,
'sale_ok': True,
'website_published': True,
})
self.env['res.partner'].create({
@ -42,7 +49,85 @@ class TestWebsiteSaleMail(HttpCase):
self.start_tour("/", 'shop_mail', login="admin")
new_mail = self.env['mail.mail'].search([('create_date', '>=', start_time),
('body_html', 'ilike', 'https://my-test-domain.com')],
order='create_date DESC', limit=1)
order='create_date DESC', limit=None)
self.assertTrue(new_mail)
self.assertIn('Your', new_mail.body_html)
self.assertIn('order', new_mail.body_html)
def test_shop_product_mail_action_redirection(self):
product_template = self.env['product.template'].create({
'name': "test product template",
'sale_ok': True,
'website_published': True,
})
url = f'/mail/view?model=product.template&res_id={product_template.id}'
shop_url = f'/shop/test-product-template-{product_template.id}'
with self.subTest(user='admin'):
self.authenticate('admin', 'admin')
res = self.url_open(url)
self.assertEqual(res.status_code, 200)
self.assertTrue(res.request.path_url.startswith('/odoo/product.template'))
with self.subTest(user='portal'):
self.authenticate('portal', 'portal')
res = self.url_open(url)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.request.path_url, shop_url)
with self.subTest(user=None):
self.authenticate(None, None)
res = self.url_open(url)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.request.path_url, shop_url)
@tagged('post_install', '-at_install', 'mail_thread')
class TestWebsiteSaleMails(MailCommon, WebsiteSaleCommon):
def test_salesman_assignation(self):
self.website.salesperson_id = self.user_admin
MailThread = odoo.addons.mail.models.mail_thread.MailThread
base_method = MailThread._message_create
superuser = self.env['res.users'].browse(SUPERUSER_ID)
# Public user
with patch.object(
MailThread, '_message_create', autospec=True, side_effect=base_method
) as patcher:
self.cart.with_user(self.public_user).with_context(tracking_disable=False, mail_no_track=False).sudo().action_confirm()
patcher.assert_called()
order, msg_values = None, {}
for (record, call_args), _whatever in patcher.call_args_list:
if record._name == 'sale.order':
order = record
msg_values = call_args[0]
break
self.assertEqual(order, self.cart)
self.assertEqual(msg_values['author_id'], superuser.partner_id.id)
self.assertEqual(msg_values['partner_ids'], self.partner_admin.ids)
self.assertEqual(msg_values['subject'], f"You have been assigned to {order.name}")
# Portal user
user_portal = self._create_portal_user()
portal_partner = user_portal.partner_id
portal_user_cart = self.cart.copy({'partner_id': portal_partner.id, 'user_id': False})
with patch.object(
MailThread, '_message_create', autospec=True, side_effect=base_method
) as patcher:
portal_user_cart.with_user(user_portal).with_context(tracking_disable=False, mail_no_track=False).sudo().action_confirm()
patcher.assert_called()
order, msg_values = None, {}
for (record, call_args), _whatever in patcher.call_args_list:
if record._name == 'sale.order':
order = record
msg_values = call_args[0]
break
self.assertEqual(order, portal_user_cart)
self.assertEqual(msg_values['author_id'], superuser.partner_id.id)
self.assertEqual(msg_values['partner_ids'], self.partner_admin.ids)
self.assertEqual(msg_values['subject'], f"You have been assigned to {order.name}")

View file

@ -0,0 +1,33 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.sale.tests.common import SaleCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestWebsiteSaleOrderEmailTemplate(SaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].create({
'name': 'Test Website for Email Template',
})
cls.sale_order.website_id = cls.website.id
def test_website_specific_confirmation_template_is_used(self):
"""Ensure _get_confirmation_template returns the website-specific template when set."""
template = self.env['mail.template'].create({
'name': "Website Custom Confirmation Template",
'model_id': self.env.ref('sale.model_sale_order').id,
'subject': "Website Confirmation",
'body_html': '<p>Hello</p>',
})
self.website.confirmation_email_template_id = template
with patch.object(self.env.registry['sale.order'], '_send_order_notification_mail') as mock:
self.sale_order._send_order_confirmation_mail()
mock.assert_called_once()
returned_confirmation_template = mock.call_args[0][0]
self.assertEqual(returned_confirmation_template, template)

View file

@ -1,19 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import datetime, timedelta
from freezegun import freeze_time
from unittest.mock import patch
from freezegun import freeze_time
from odoo.fields import Command
from odoo.tests import tagged, TransactionCase
from odoo.tests import tagged
from odoo.tools import SQL
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.addons.website.tools import MockRequest
_logger = logging.getLogger(__name__)
from odoo.addons.base.tests.common import HttpCaseWithUserPortal, TransactionCaseWithUserDemo
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
r''' /!\/!\
Calling `get_pricelist_available` after setting `property_product_pricelist` on
@ -35,80 +32,85 @@ Try to keep one call to `get_pricelist_available` by test method.
@tagged('post_install', '-at_install')
class TestWebsitePriceList(TransactionCase):
class TestWebsitePriceList(WebsiteSaleCommon):
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
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.partner_id.country_id = False # Remove country to avoid property pricelist computed.
(self.env['product.pricelist'].search([]) - self.env.ref('product.list0')).write({'website_id': False, 'active': False})
self.benelux = self.env['res.country.group'].create({
cls.pricelist.name = "Public Pricelist" # reduce diff in existing tests
cls.country_be = cls.env.ref('base.be')
cls.benelux = cls.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)]
'country_ids': [Command.set((cls.country_be + cls.env.ref('base.lu') + cls.env.ref('base.nl')).ids)]
})
self.list_benelux = self.env['product.pricelist'].create({
cls.curr_eur = cls._enable_currency('EUR')
cls.list_benelux = cls.env['product.pricelist'].create({
'name': 'Benelux',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(4, self.benelux.id)],
'website_id': cls.website.id,
'country_group_ids': [Command.link(cls.benelux.id)],
'currency_id': cls.curr_eur.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,
'item_ids': [
Command.create({
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
}),
]
})
self.list_christmas = self.env['product.pricelist'].create({
cls.europe = cls.env.ref('base.europe')
cls.list_christmas = cls.env['product.pricelist'].create({
'name': 'Christmas',
'selectable': False,
'website_id': self.website.id,
'country_group_ids': [(4, self.env.ref('base.europe').id)],
'website_id': cls.website.id,
'country_group_ids': [Command.link(cls.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,
'item_ids': [
Command.create({
'compute_price': 'formula',
'base': 'list_price',
'price_discount': 20,
}),
]
})
list_europe = self.env['product.pricelist'].create({
cls.list_europe = cls.env['product.pricelist'].create({
'name': 'EUR',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(4, self.env.ref('base.europe').id)],
'website_id': cls.website.id,
'country_group_ids': [Command.link(cls.europe.id)],
'sequence': 3,
'currency_id': self.env.ref('base.EUR').id,
'currency_id': cls.curr_eur.id,
'item_ids': [
Command.create({
'compute_price': 'formula',
'base': 'list_price',
}),
]
})
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({
ca_group = cls.env['res.country.group'].create({
'name': 'Canada',
'country_ids': [(6, 0, [self.ref('base.ca')])]
'country_ids': [Command.set([cls.env.ref('base.ca').id])]
})
self.env['product.pricelist'].create({
cls.env['product.pricelist'].create({
'name': 'Canada',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(6, 0, [ca_group.id])],
'website_id': cls.website.id,
'country_group_ids': [Command.set(ca_group.ids)],
'sequence': 10
})
self.args = {
cls.args = {
'show': False,
'current_pl': False,
}
def setUp(self):
super().setUp()
patcher = patch('odoo.addons.website_sale.models.website.Website.get_pricelist_available', wraps=self._get_pricelist_available)
self.startPatcher(patcher)
@ -162,11 +164,12 @@ class TestWebsitePriceList(TransactionCase):
def test_get_pricelist_available_promocode(self):
christmas_pl = self.list_christmas.id
# Christmas Pricelist only available for EU countries
country_list = {
False: True,
'BE': True,
'IT': True,
'US': True,
'US': False,
'CA': False
}
@ -181,7 +184,7 @@ class TestWebsitePriceList(TransactionCase):
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
self.env.user.partner_id.country_id = self.country_be # Add EUR pricelist auto
current_pl = False
country_list = {
@ -197,16 +200,16 @@ class TestWebsitePriceList(TransactionCase):
% (country, len(pls), pls.mapped('name'), len(result), result))
def test_pricelist_combination(self):
# Enable discounts to view discount in sale_order
self.env.user.group_ids += self.env.ref('sale.group_discount_per_so_line')
product = self.env['product.product'].create({
'name': 'Super Product',
'list_price': 100,
'taxes_id': False,
})
current_website = self.env['website'].get_current_website()
website_pricelist = current_website.get_current_pricelist()
website_pricelist.write({
'discount_policy': 'with_discount',
'item_ids': [(5, 0, 0), (0, 0, {
self.pricelist.write({
'item_ids': [Command.clear(), Command.create({
'applied_on': '1_product',
'product_tmpl_id': product.product_tmpl_id.id,
'min_quantity': 500,
@ -216,34 +219,33 @@ class TestWebsitePriceList(TransactionCase):
})
promo_pricelist = self.env['product.pricelist'].create({
'name': 'Super Pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'item_ids': [Command.create({
'applied_on': '1_product',
'product_tmpl_id': product.product_tmpl_id.id,
'base': 'pricelist',
'base_pricelist_id': website_pricelist.id,
'compute_price': 'formula',
'price_discount': 25
'base_pricelist_id': self.pricelist.id,
'compute_price': 'percentage',
'percent_price': 25,
})]
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [(0, 0, {
'website_id': self.website.id,
'pricelist_id': self.pricelist.id,
'order_line': [Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
'tax_id': False,
})]
'tax_ids': False,
})],
})
sol = so.order_line
self.assertEqual(sol.price_total, 100.0)
so.pricelist_id = promo_pricelist
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=500)
self.assertEqual(sol.price_unit, 37.0, 'Both reductions should be applied')
self.assertEqual(sol.price_reduce, 27.75, 'Both reductions should be applied')
so._cart_update_line_quantity(line_id=sol.id, quantity=500)
self.assertEqual(sol.price_unit, 100.0, 'Both reductions should be applied')
self.assertEqual(sol.discount, 72.25, 'Both reductions should be applied')
self.assertEqual(sol.price_total, 13875)
def test_pricelist_with_no_list_price(self):
@ -252,58 +254,37 @@ class TestWebsitePriceList(TransactionCase):
'list_price': 0,
'taxes_id': False,
})
current_website = self.env['website'].get_current_website()
website_pricelist = current_website.get_current_pricelist()
website_pricelist.write({
'discount_policy': 'without_discount',
'item_ids': [(5, 0, 0), (0, 0, {
'applied_on': '1_product',
'product_tmpl_id': product.product_tmpl_id.id,
'min_quantity': 0,
'compute_price': 'fixed',
'fixed_price': 10,
})]
self.pricelist.write({
'item_ids': [
Command.clear(),
Command.create({
'applied_on': '1_product',
'product_tmpl_id': product.product_tmpl_id.id,
'min_quantity': 0,
'compute_price': 'fixed',
'fixed_price': 10,
}),
]
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [(0, 0, {
'website_id': self.website.id,
'pricelist_id': self.pricelist.id,
'order_line': [Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 5,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
'tax_id': False,
'tax_ids': False,
})]
})
sol = so.order_line
self.assertEqual(sol.price_total, 0)
so.pricelist_id = website_pricelist
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=6)
so._cart_update_line_quantity(line_id=sol.id, quantity=6)
self.assertEqual(sol.price_unit, 10.0, 'Pricelist price should be applied')
self.assertEqual(sol.price_reduce, 10.0, 'Pricelist price should be applied')
self.assertEqual(sol.discount, 0, 'Pricelist price should be applied')
self.assertEqual(sol.price_total, 60.0)
def test_get_right_discount(self):
""" Test that `_get_sales_prices` from `product_template`
returns a dict with just `price_reduce` (no discount) as key
when the product is tax included.
"""
self.env.company.country_id = self.env.ref('base.us')
tax = self.env['account.tax'].create({
'name': "Tax 10",
'amount': 10,
})
product = self.env['product.template'].create({
'name': 'Event Product',
'list_price': 10.0,
'taxes_id': tax,
})
prices = product._get_sales_prices(self.list_christmas)
self.assertFalse('base_price' in prices[product.id])
def test_pricelist_item_based_on_cost_for_templates(self):
""" Test that `_get_sales_prices` from `product_template` computes the correct price when
the pricelist item is based on the cost of the product.
@ -318,31 +299,78 @@ class TestWebsitePriceList(TransactionCase):
})
pa = self.env['product.attribute'].create({'name': 'Attribute'})
pav1 = self.env['product.attribute.value'].create({'name': 'Value1', 'attribute_id': pa.id})
pav2 = self.env['product.attribute.value'].create({'name': 'Value2', 'attribute_id': pa.id})
pav1, pav2 = self.env['product.attribute.value'].create([
{'name': 'Value1', 'attribute_id': pa.id},
{'name': 'Value2', 'attribute_id': pa.id},
])
product_template = self.env['product.template'].create({
'name': 'Product Template', 'list_price': 10.0, 'standard_price': 5.0
})
self.assertEqual(product_template.standard_price, 5)
price = product_template._get_sales_prices(pricelist)[product_template.id]['price_reduce']
msg = "Template has no variants, the price should be computed based on the template's cost."
self.assertEqual(price, 4.5, msg)
with MockRequest(
self.env, website=self.website, website_sale_current_pl=pricelist.id
) as request:
self.assertEqual(request.pricelist, pricelist)
price = product_template._get_sales_prices(self.website)[product_template.id]['price_reduce']
msg = "Template has no variants, the price should be computed based on the template's cost."
self.assertEqual(price, 4.5, msg)
product_template.attribute_line_ids = [Command.create({
'attribute_id': pa.id, 'value_ids': [Command.set([pav1.id, pav2.id])]
})]
msg = "Product template with variants should have no cost."
self.assertEqual(product_template.standard_price, 0, msg)
self.assertEqual(product_template.product_variant_ids[0].standard_price, 0)
product_template.attribute_line_ids = [Command.create({
'attribute_id': pa.id, 'value_ids': [Command.set([pav1.id, pav2.id])]
})]
msg = "Product template with variants should have no cost."
self.assertEqual(product_template.standard_price, 0, msg)
self.assertEqual(product_template.product_variant_ids[0].standard_price, 0)
price = product_template._get_sales_prices(pricelist)[product_template.id]['price_reduce']
msg = "Template has variants, the price should be computed based on the 1st variant's cost."
self.assertEqual(price, 0, msg)
price = product_template._get_sales_prices(self.website)[product_template.id]['price_reduce']
msg = "Template has variants, the price should be computed based on the 1st variant's cost."
self.assertEqual(price, 0, msg)
product_template.product_variant_ids[0].standard_price = 20
price = product_template._get_sales_prices(pricelist)[product_template.id]['price_reduce']
self.assertEqual(price, 18, msg)
product_template.product_variant_ids[0].standard_price = 20
price = product_template._get_sales_prices(self.website)[product_template.id]['price_reduce']
self.assertEqual(price, 18, msg)
def test_base_price_with_discount_on_pricelist_tax_included(self):
"""
Tests that the base price of a product with tax included
and discount from a price list is correctly displayed in the shop
ex: A product with a price of $61.98 ($75 tax incl. of 21%) and a discount of 20%
should display the base price of $75
"""
self.env['res.config.settings'].create({ # Set Settings:
'show_line_subtotals_tax_selection': 'tax_included', # Set "Tax Included" on the "Display Product Prices"
'group_product_price_comparison': True, # price comparison
}).execute()
product_tmpl = self.env['product.template'].create({
'name': 'Test Product',
'type': 'consu',
'list_price': 61.98, # 75 tax incl.
'taxes_id': [
Command.create({
'name': '21%',
'type_tax_use': 'sale',
'amount': 21,
})
],
'is_published': True,
})
self.pricelist.write({
'item_ids': [Command.create({
'percent_price': 20,
'compute_price': 'percentage',
'product_tmpl_id': product_tmpl.id,
})],
})
with MockRequest(
self.website.env, website=self.website, website_sale_current_pl=self.pricelist.id
) as request:
self.assertEqual(request.pricelist, self.pricelist)
res = product_tmpl._get_sales_prices(self.website)
self.assertEqual(res[product_tmpl.id]['base_price'], 75)
def test_pricelist_item_validity_period(self):
""" Test that if a cart was created before a validity period,
@ -364,8 +392,6 @@ class TestWebsitePriceList(TransactionCase):
'list_price': 100,
'taxes_id': False,
})
current_website = self.env['website'].get_current_website()
current_website.pricelist_id = pricelist
with freeze_time(today) as frozen_time:
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
@ -374,21 +400,53 @@ class TestWebsitePriceList(TransactionCase):
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'product_uom_id': product.uom_id.id,
'price_unit': product.list_price,
'tax_id': False,
})],
'website_id': current_website.id,
'website_id': self.website.id,
})
sol = so.order_line
self.assertEqual(sol.price_total, 100.0)
frozen_time.move_to(tomorrow + timedelta(seconds=10))
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=2)
so._cart_update_line_quantity(line_id=sol.id, quantity=2)
self.assertEqual(sol.price_unit, 80.0, 'Reduction should be applied')
self.assertEqual(sol.price_total, 160)
def test_pricelist_anonymous_user(self):
list_benelux_2 = self.list_benelux.sudo().copy({
'name': 'Benelux 2',
'item_ids': [
Command.create({
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 20,
}),
]
})
order_sudo = self.env['sale.order'].sudo().create({
'partner_id': self.public_partner.id,
'pricelist_id': list_benelux_2.id,
'order_line': [Command.create({
'name': self.product.name,
'product_id': self.product.id,
})],
})
# Creating partner with address of belgium
partner = self.env['res.partner'].create({
'name': 'Test Partner',
'company_id': False,
'country_id': self.env.ref('base.be').id,
})
website = self.website.with_user(self.public_user)
with MockRequest(
website.env, website=website,
website_sale_current_pl=list_benelux_2.id,
website_sale_selected_pl_id=list_benelux_2.id
) as request:
order_sudo._update_address({'partner_id': partner.id})
self.assertEqual(order_sudo.pricelist_id, list_benelux_2)
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():
@ -398,66 +456,68 @@ def simulate_frontend_context(self, website_id=1):
@tagged('post_install', '-at_install')
class TestWebsitePriceListAvailable(TransactionCase):
def setUp(self):
super(TestWebsitePriceListAvailable, self).setUp()
Pricelist = self.env['product.pricelist']
Website = self.env['website']
class TestWebsitePriceListAvailable(WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._enable_pricelists()
Pricelist = cls.env['product.pricelist']
Website = cls.env['website']
# Set up 2 websites
self.website = Website.browse(1)
self.website2 = Website.create({'name': 'Website 2'})
cls.website2 = Website.create({'name': 'Website 2'})
# Remove existing pricelists and create new ones
existing_pricelists = Pricelist.search([])
self.backend_pl = Pricelist.create({
cls.backend_pl = Pricelist.create({
'name': 'Backend Pricelist',
'website_id': False,
})
self.generic_pl_select = Pricelist.create({
cls.generic_pl_select = Pricelist.create({
'name': 'Generic Selectable Pricelist',
'selectable': True,
'website_id': False,
})
self.generic_pl_code = Pricelist.create({
cls.generic_pl_code = Pricelist.create({
'name': 'Generic Code Pricelist',
'code': 'GENERICCODE',
'website_id': False,
})
self.generic_pl_code_select = Pricelist.create({
cls.generic_pl_code_select = Pricelist.create({
'name': 'Generic Code Selectable Pricelist',
'code': 'GENERICCODESELECT',
'selectable': True,
'website_id': False,
})
self.w1_pl = Pricelist.create({
cls.w1_pl = Pricelist.create({
'name': 'Website 1 Pricelist',
'website_id': self.website.id,
'website_id': cls.website.id,
})
self.w1_pl_select = Pricelist.create({
cls.w1_pl_select = Pricelist.create({
'name': 'Website 1 Pricelist Selectable',
'website_id': self.website.id,
'website_id': cls.website.id,
'selectable': True,
})
self.w1_pl_code_select = Pricelist.create({
cls.w1_pl_code_select = Pricelist.create({
'name': 'Website 1 Pricelist Code Selectable',
'website_id': self.website.id,
'website_id': cls.website.id,
'code': 'W1CODESELECT',
'selectable': True,
})
self.w1_pl_code = Pricelist.create({
cls.w1_pl_code = Pricelist.create({
'name': 'Website 1 Pricelist Code',
'website_id': self.website.id,
'website_id': cls.website.id,
'code': 'W1CODE',
})
self.w2_pl = Pricelist.create({
cls.w2_pl = Pricelist.create({
'name': 'Website 2 Pricelist',
'website_id': self.website2.id,
'website_id': cls.website2.id,
})
existing_pricelists.write({'active': False})
self.website = self.env['website'].browse(1)
existing_pricelists.action_archive()
def setUp(self):
super().setUp()
simulate_frontend_context(self)
def test_get_pricelist_available(self):
@ -478,7 +538,7 @@ class TestWebsitePriceListAvailable(TransactionCase):
# 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')
public_partner = self.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`")
@ -487,9 +547,36 @@ class TestWebsitePriceListAvailable(TransactionCase):
@tagged('post_install', '-at_install')
class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
def setUp(self):
super(TestWebsitePriceListAvailableGeoIP, self).setUp()
super().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()
self.env.invalidate_all()
for field in self.env.registry.many2one_company_dependents['res.partner']:
self.env.cr.execute(SQL(
"""
UPDATE %(table)s
SET %(field)s = (
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(%(field)s)
WHERE value != %(id_)s
)
WHERE %(field)s IS NOT NULL
""",
table=SQL.identifier(self.env[field.model_name]._table),
field=SQL.identifier(field.name),
id_=SQL('to_jsonb(%s::int)', self.env.user.partner_id.id),
))
if fields_ := self.env.registry.many2one_company_dependents['res.partner']:
field_ids = [self.env['ir.model.fields']._get_ids(field.model_name).get(field.name) for field in fields_]
self.env.cr.execute(SQL(
"""
DELETE FROM ir_default
WHERE field_id IN %(field_ids)s
AND json_value = %(id_text)s
""",
field_ids=tuple(field_ids),
id_text=str(self.env.user.partner_id.id),
))
self.env.registry.clear_cache()
# set different country groups on pricelists
c_EUR = self.env.ref('base.europe')
@ -499,14 +586,17 @@ class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
})
self.BE = self.env.ref('base.be')
self.US = self.env.ref('base.us')
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])]})
c_BE, c_NL = self.env['res.country.group'].create([
{'name': 'Belgium', 'country_ids': [(6, 0, [self.BE.id])]},
{'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])]})
(self.backend_pl + self.generic_pl_select + self.generic_pl_code + self.w1_pl_select).write({'country_group_ids': [Command.set(c_BE.ids)]})
(self.generic_pl_code_select + self.w1_pl + self.w2_pl).write({'country_group_ids': [Command.set(c_BENELUX.ids)]})
(self.w1_pl_code).write({'country_group_ids': [Command.set(c_EUR.ids)]})
(self.w1_pl_code_select).write({'country_group_ids': [Command.set(c_NL.ids)]})
# pricelist | selectable | website | code | country group |
# ----------------------------------------------------------------------|
@ -524,7 +614,7 @@ class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
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)
# Test get all available pricelists with geoip and no partner pricelist
# property_product_pricelist will also be returned in the available pricelists
self.website1_be_pl += self.env.user.partner_id.property_product_pricelist
@ -534,14 +624,14 @@ class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
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
# Test get all available pricelists with geoip and a partner pricelist not website compliant
self.env.user.partner_id.property_product_pricelist = self.backend_pl
with patch('odoo.addons.website_sale.models.website.Website._get_geoip_country_code', return_value=self.BE.code):
pls = self.website.get_pricelist_available()
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)
# Test get all available pricelists with geoip and a partner pricelist website compliant (but not geoip compliant)
self.env.user.partner_id.property_product_pricelist = self.w1_pl_code_select
with patch('odoo.addons.website_sale.models.website.Website._get_geoip_country_code', return_value=self.BE.code):
pls = self.website.get_pricelist_available()
@ -554,11 +644,45 @@ class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
pls_to_return += self.env.user.partner_id.property_product_pricelist
current_pl = self.w1_pl_code
with patch('odoo.addons.website_sale.models.website.Website._get_geoip_country_code', return_value=self.BE.code), \
patch('odoo.addons.website_sale.models.website.Website._get_cached_pricelist_id', return_value=current_pl.id):
with (
patch('odoo.addons.website_sale.models.website.Website._get_geoip_country_code', return_value=self.BE.code),
MockRequest(self.env, website=self.website, website_sale_current_pl=current_pl.id),
):
pls = self.website.get_pricelist_available(show_visible=True)
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")
def test_get_pricelist_available_geoip5(self):
# Test get all available pricelists with geoip for a country not existing in any pricelists
with patch('odoo.addons.website_sale.models.website.Website._get_geoip_country_code', return_value=self.US.code):
pricelists = self.website.get_pricelist_available()
self.assertFalse(pricelists, "Pricelists specific to NL and BE should not be returned for US.")
def test_get_pricelist_available_geoip6(self):
"""Remove country group from certain pricelists, and check that pricelists
with country group get prioritized when geoip is available."""
exclude = self.backend_pl + self.generic_pl_code + self.w1_pl_select + self.w1_pl_code
exclude.country_group_ids = False
self.website1_be_pl -= exclude
with patch(
'odoo.addons.website_sale.models.website.Website._get_geoip_country_code',
return_value=self.BE.code,
):
pls = self.website.get_pricelist_available()
for pl in pls:
self.assertIn(
self.BE,
pl.country_group_ids.country_ids,
"Pricelists should have a country group that includes BE",
)
self.assertEqual(
pls,
self.website1_be_pl,
"Only pricelists for BE and accessible on website should be returned",
)
@tagged('post_install', '-at_install')
class TestWebsitePriceListHttp(HttpCaseWithUserPortal):
@ -595,7 +719,7 @@ class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
- Set website's company to company 2
- Demo user will still be in company 1
'''
super(TestWebsitePriceListMultiCompany, self).setUp()
super().setUp()
self.demo_user = self.user_demo
@ -607,9 +731,6 @@ class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
Website = self.env['website']
self.website = self.env.ref('website.default_website')
self.website.company_id = self.company2
# Delete unused website, it will make PL manipulation easier, avoiding
# UserError being thrown when a website wouldn't have any PL left.
Website.search([('id', '!=', self.website.id)]).unlink()
self.website2 = Website.create({
'name': 'Website 2',
'company_id': self.company1.id,
@ -633,16 +754,11 @@ class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
# 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.")
# ---------------------------------- 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
# property_product_pricelist has been cached
field = self.env['res.partner']._fields['property_product_pricelist']
cache_rp1 = self.env.cache.get(self.demo_user.partner_id.with_company(self.company1.id), field)
cache_rp2 = self.env.cache.get(self.demo_user.partner_id.with_company(self.company2.id), field)
self.assertEqual((cache_rp1, cache_rp2), (self.c1_pl.id, self.c2_pl.id), "Ensure the pricelist is the company specific one.")
def test_property_product_pricelist_multi_company(self):
''' Test that the `property_product_pricelist` of `res.partner` is read
@ -651,13 +767,13 @@ class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
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
It should return the data 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.
'''
simulate_frontend_context(self, self.website.id)
# First check: It should return ir.property,4 as company_id is
# First check: It should return c2_pl 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)
@ -689,12 +805,14 @@ class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
# self.c2_pl | self.website | self.company2 |
# c2_pl2 | self.website | self.company2 |
self.demo_user.groups_id += self.env.ref('sales_team.group_sale_manager')
self.demo_user.group_ids += self.env.ref('sales_team.group_sale_manager')
# The test is here: while having access only to self.company2 records,
# archive should not raise an error
self.demo_user.group_ids += self.env.ref('product.group_product_manager')
self.c2_pl.with_user(self.demo_user).with_context(allowed_company_ids=self.company2.ids).write({'active': False})
@tagged('post_install', '-at_install')
class TestWebsiteSaleSession(HttpCaseWithUserPortal):
@ -703,6 +821,9 @@ class TestWebsiteSaleSession(HttpCaseWithUserPortal):
The objective is to verify that the pricelist
changes correctly according to the user.
"""
self.env.user.write(
{'group_ids': [Command.link(self.env.ref('product.group_product_pricelist').id)]}
)
website = self.env.ref('website.default_website')
test_user = self.env['res.users'].create({
'name': 'Toto',
@ -723,4 +844,4 @@ class TestWebsiteSaleSession(HttpCaseWithUserPortal):
'code': 'User_pricelist',
})
test_user.partner_id.property_product_pricelist = user_pricelist
self.start_tour("/shop", 'website_sale_shop_pricelist_tour', login="")
self.start_tour("/shop", 'website_sale.website_sale_shop_pricelist_tour', login="")

View file

@ -1,144 +0,0 @@
# coding: utf-8
import itertools
from odoo.tests import tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon
from odoo.addons.website_sale.controllers.main import WebsiteSale
@tagged('post_install', '-at_install')
class WebsiteSaleProductTests(TestSaleProductAttributeValueCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.WebsiteSaleController = WebsiteSale()
cls.website = cls.env.ref('website.default_website')
cls.website.company_id = cls.env.company
cls.tax_5 = cls.env['account.tax'].create({
'name': '5% Tax',
'amount_type': 'percent',
'amount': 5,
'price_include': False,
'include_base_amount': False,
'type_tax_use': 'sale',
})
cls.tax_10 = cls.env['account.tax'].create({
'name': '10% Tax',
'amount_type': 'percent',
'amount': 10,
'price_include': False,
'include_base_amount': False,
'type_tax_use': 'sale',
})
cls.tax_15 = cls.env['account.tax'].create({
'name': '15% Tax',
'amount_type': 'percent',
'amount': 15,
'price_include': False,
'include_base_amount': False,
'type_tax_use': 'sale',
})
cls.fiscal_country = cls.env['res.country'].create({
'name': "Super Fiscal Position",
'code': 'SFP',
})
cls.product = cls.env['product.product'].create({
'name': 'Super Product',
'list_price': 100.0,
})
def test_website_sale_contextual_price(self):
contextual_price = self.computer._get_contextual_price()
self.assertEqual(0.0, contextual_price, "With no pricelist context, the contextual price should be 0.")
current_website = self.env['website'].get_current_website()
pricelist = current_website.get_current_pricelist()
# 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
currency_ratio = 2
pricelist.currency_id = self._setup_currency(currency_ratio)
with MockRequest(self.env, website=self.website):
contextual_price = self.computer._get_contextual_price()
self.assertEqual(
2000.0 * currency_ratio * discount_rate, contextual_price,
"With a website pricelist context, the contextual price should be the one defined for the website's pricelist."
)
def test_get_contextual_price_tax_selection(self):
"""
`_get_contextual_price_tax_selection` is used to display the price on the website (e.g. in the carousel).
We test that the contextual price is correctly computed. That is, it is coherent with the price displayed on the product when in the cart.
"""
param_main_product_tax_included = [True, False]
param_show_line_subtotals_tax_selection = ['tax_included', 'tax_excluded']
param_extra_tax = [False, 'included', 'excluded']
param_fpos = [False, 'to_tax_excluded']
parameters = itertools.product(param_main_product_tax_included, param_show_line_subtotals_tax_selection, param_extra_tax, param_fpos)
for main_product_tax_included, show_line_subtotals_tax_selection, extra_tax, fpos in parameters:
with self.subTest(main_product_tax_included=main_product_tax_included, show_line_subtotals_tax_selection=show_line_subtotals_tax_selection, extra_tax=extra_tax, fpos=fpos):
# set "show_line_subtotals_tax_selection" parameter
config = self.env['res.config.settings'].create({})
config.show_line_subtotals_tax_selection = show_line_subtotals_tax_selection
config.execute()
# set "main_product_tax_included" parameter
if main_product_tax_included:
self.tax_15.price_include = True
self.tax_15.include_base_amount = True
self.product.taxes_id = self.tax_15
tax_ids = [self.tax_15.id]
# set "extra_tax" parameter
if extra_tax:
if extra_tax == 'included':
self.tax_5.price_include = True
self.tax_5.include_base_amount = True
tax_ids.append(self.tax_5.id)
# set "fpos" parameter
if fpos:
if fpos == 'to_tax_included':
self.tax_10.price_include = True
self.tax_10.include_base_amount = True
fiscal_position = self.env['account.fiscal.position'].create({
'name': 'Super Fiscal Position',
'auto_apply': True,
'country_id': self.fiscal_country.id,
})
self.env['account.fiscal.position.tax'].create({
'position_id': fiscal_position.id,
'tax_src_id': self.tax_15.id,
'tax_dest_id': self.tax_10.id,
})
self.env.user.partner_id.country_id = self.fiscal_country
# define the website pricelist
current_website = self.env['website'].get_current_website()
pricelist = current_website.get_current_pricelist()
pricelist.currency_id = self.product.currency_id
self.env['product.pricelist.item'].create({
'price_discount': 0,
'compute_price': 'formula',
'pricelist_id': pricelist.id,
})
with MockRequest(self.env, website=self.website, website_sale_current_pl=pricelist.id):
contextual_price = self.product._get_contextual_price_tax_selection()
self.WebsiteSaleController.cart_update_json(product_id=self.product.id, add_qty=1)
sale_order = self.website.sale_get_order()
if show_line_subtotals_tax_selection == 'tax_included':
self.assertAlmostEqual(sale_order.amount_total, contextual_price)
else:
self.assertAlmostEqual(sale_order.amount_untaxed, contextual_price)

View file

@ -1,42 +1,46 @@
# -*- 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 import Command
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.product.tests.test_product_attribute_value_config import (
TestProductAttributeValueCommon,
)
from odoo.addons.website_sale.tests.common import MockRequest
@tagged('post_install', '-at_install', 'product_attribute')
class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
class TestWebsiteSaleProductAttributeValueConfig(AccountTestInvoicingCommon, TestProductAttributeValueCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
# Use the testing environment.
cls.env['website'].get_current_website().company_id = cls.env.company
cls.computer.company_id = cls.env.company
cls.computer = cls.computer.with_env(cls.env)
cls.other_currency = cls.setup_other_currency('GBP')
def test_get_combination_info(self):
# Setup pricelist: make sure the pricelist has a 10% discount
pricelist = self.env['product.pricelist'].create({
'name': "test_get_combination_info",
'currency_id': self.currency_data['currency'].id,
'discount_policy': 'with_discount',
'company_id': self.env.company.id,
'item_ids': [Command.create({
'price_discount': 10,
'compute_price': 'formula',
})],
})
# Setup website.
website = self.env['website'].create({
'name': "Test website",
'company_id': self.env.company.id,
'user_id': self.env.user.id,
'pricelist_ids': [Command.set(pricelist.ids)],
})
# Setup pricelist: make sure the pricelist has a 10% discount
self.env['product.pricelist'].search([]).action_archive()
pricelist = self.env['product.pricelist'].create({
'name': "test_get_combination_info",
'currency_id': self.other_currency.id,
'company_id': self.env.company.id,
'item_ids': [Command.create({
'percent_price': 10,
'compute_price': 'percentage',
})],
'website_id': website.id,
})
# Setup product with 15% tax.
@ -51,42 +55,22 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo
currency_ratio = 2
# CASE: B2B setting (default)
combination_info = product_template._get_combination_info(pricelist=pricelist)
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['price_extra'], 222 * currency_ratio)
self.assertEqual(combination_info['has_discounted_price'], False)
with MockRequest(product_template.env, website=website, website_sale_current_pl=pricelist.id):
combination_info = product_template._get_combination_info()
self.assertEqual(combination_info['price'], 2222 * discount_rate * currency_ratio)
self.assertEqual(combination_info['list_price'], 2222 * currency_ratio)
self.assertEqual(combination_info['has_discounted_price'], True)
# CASE: B2C 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_excluded.users -= self.env.user
group_tax_included.users |= self.env.user
# CASE: B2C setting
website.show_line_subtotals_tax_selection = 'tax_included'
combination_info = product_template._get_combination_info(pricelist=pricelist)
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['price_extra'], round(222 * currency_ratio * tax_ratio, 2))
self.assertEqual(combination_info['has_discounted_price'], False)
# CASE: pricelist 'without_discount'
pricelist.discount_policy = 'without_discount'
combination_info = product_template._get_combination_info(pricelist=pricelist)
self.assertEqual(combination_info['price'], pricelist.currency_id.round(2222 * discount_rate * currency_ratio * tax_ratio), 0)
self.assertEqual(combination_info['list_price'], pricelist.currency_id.round(2222 * currency_ratio * tax_ratio), 0)
self.assertEqual(combination_info['price_extra'], pricelist.currency_id.round(222 * currency_ratio * tax_ratio), 0)
self.assertEqual(combination_info['has_discounted_price'], True)
combination_info = product_template._get_combination_info()
self.assertEqual(combination_info['price'], 2222 * discount_rate * currency_ratio * tax_ratio)
self.assertAlmostEqual(combination_info['list_price'], 2222 * currency_ratio * tax_ratio)
self.assertEqual(combination_info['has_discounted_price'], True)
def test_get_combination_info_with_fpos(self):
# Setup product.
self.env.user.partner_id.write({
'country_id': False,
'property_product_pricelist': self.env.ref('product.list0').id,
})
current_website = self.env['website'].get_current_website()
pricelist = current_website.get_current_pricelist()
(self.env['product.pricelist'].search([]) - pricelist).write({'active': False})
product = self.env['product.template'].create({
'name': 'Test Product',
'list_price': 2000,
@ -94,10 +78,19 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo
'company_id': self.env.company.id,
})
# Setup website.
website = self.env['website'].create({
'name': "Test website",
'company_id': self.env.company.id,
'user_id': self.env.user.id,
})
# Setup pricelist: make sure the pricelist has a 10% discount
pricelist = self.env['product.pricelist'].create({
self.env['product.pricelist'].search([]).action_archive()
self.env['product.pricelist'].create({
'name': "test_get_combination_info",
'company_id': self.env.company.id,
'website_id': website.id,
'item_ids': [Command.create({
'applied_on': "1_product",
'base': "list_price",
@ -107,14 +100,6 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo
})],
})
# Setup website.
website = self.env['website'].create({
'name': "Test website",
'company_id': self.env.company.id,
'user_id': self.env.user.id,
'pricelist_ids': [Command.set(pricelist.ids)],
})
product = product.with_context(website_id=website.id)
# Setup product attributes.
@ -126,192 +111,67 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo
computer_ssd_attribute_lines.product_template_value_ids[0].price_extra = 200
# 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
website.show_line_subtotals_tax_selection = 'tax_included'
combination_info = product._get_combination_info(pricelist=pricelist)
with MockRequest(product.env, website=website):
combination_info = product._get_combination_info()
self.assertEqual(combination_info['price'], 575, "500$ + 15% tax")
self.assertEqual(combination_info['list_price'], 575, "500$ + 15% tax (2)")
self.assertEqual(combination_info['price_extra'], 230, "200$ + 15% tax")
# Setup fiscal position 15% => 0%.
us_country = self.env.ref('base.us')
tax0 = self.env['account.tax'].create({'name': "Test tax 0", 'amount': 0})
self.env['account.fiscal.position'].create({
jp_country = self.env.ref('base.jp')
fp = self.env['account.fiscal.position'].create({
'name': "test_get_combination_info_with_fpos",
'auto_apply': True,
'country_id': us_country.id,
'tax_ids': [Command.create({
'tax_src_id': self.company_data['default_tax_sale'].id,
'tax_dest_id': tax0.id,
})],
'country_id': jp_country.id,
})
tax0 = self.env['account.tax'].create({'name': "Test tax 0", 'amount': 0, 'fiscal_position_ids': [Command.link(fp.id)], 'original_tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)]})
# Now with fiscal position, taxes should be mapped
self.env.user.partner_id.country_id = us_country
combination_info = product._get_combination_info(pricelist=pricelist)
self.env.user.partner_id.country_id = jp_country
with MockRequest(product.env, website=website):
combination_info = product._get_combination_info()
self.assertEqual(combination_info['price'], 500, "500% + 0% tax (mapped from fp 15% -> 0%)")
self.assertEqual(combination_info['list_price'], 500, "500% + 0% tax (mapped from fp 15% -> 0%)")
self.assertEqual(combination_info['price_extra'], 200, "200% + 0% tax (mapped from fp 15% -> 0%)")
# Try same flow with tax included
self.company_data['default_tax_sale'].price_include = True
self.company_data['default_tax_sale'].price_include_override = 'tax_included'
# Reset / Safety check
self.env.user.partner_id.country_id = None
combination_info = product._get_combination_info(pricelist=pricelist)
with MockRequest(product.env, website=website):
combination_info = 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)")
self.assertEqual(combination_info['price_extra'], 200, "173.91$ + 15% tax")
# Now with fiscal position, taxes should be mapped
self.env.user.partner_id.country_id = us_country.id
combination_info = product._get_combination_info(pricelist=pricelist)
self.env.user.partner_id.country_id = jp_country.id
with MockRequest(product.env, website=website):
combination_info = product._get_combination_info()
self.assertEqual(round(combination_info['price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0%)")
self.assertEqual(round(combination_info['list_price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0%)")
self.assertEqual(combination_info['price_extra'], 173.91, "173.91$ + 0% tax (mapped from fp 15% -> 0%)")
# Try same flow with tax included for apply tax
tax0.write({'name': "Test tax 5", 'amount': 5, 'price_include': True})
combination_info = product._get_combination_info(pricelist=pricelist)
tax0.write({'name': "Test tax 5", 'amount': 5, 'price_include_override': 'tax_included'})
with MockRequest(product.env, website=website):
combination_info = product._get_combination_info()
self.assertEqual(round(combination_info['price'], 2), 456.52, "434.78$ + 5% tax (mapped from fp 15% -> 5% for BE)")
self.assertEqual(round(combination_info['list_price'], 2), 456.52, "434.78$ + 5% tax (mapped from fp 15% -> 5% for BE)")
self.assertEqual(combination_info['price_extra'], 182.61, "173.91$ + 5% tax (mapped from fp 15% -> 5% for BE)")
def test_hide_attribute_value_without_matching_product_variant(self):
"""Ensure attribute values are hidden if they don't have a matching product variant"""
self.ssd_attribute.preview_variants = 'visible'
@tagged('post_install', '-at_install', 'product_pricelist')
class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
def test_cart_update_with_fpos(self):
# We will test that the mapping of an 10% included tax by a 6% by a fiscal position is taken into account when updating the cart
self.env.user.partner_id.write({
'country_id': False,
'property_product_pricelist': self.env.ref('product.list0').id,
})
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'})
tax6 = self.env['account.tax'].create({'name': "Test tax 6", 'amount': 6, 'price_include': True, 'amount_type': 'percent'})
test_product = self.env['product.template'].create({
'name': 'Test Product',
'list_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,
product_template = self.env['product.template'].create({
'name': 'Test Product Template',
})
pricelist.discount_policy = 'without_discount'
# Create fiscal position mapping taxes 10% -> 6%
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': tax6.id,
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
})
sol = self.env['sale.order.line'].create({
'product_id': test_product.product_variant_id.id,
'order_id': so.id,
})
self.assertEqual(round(sol.price_total), 55.0, "110$ with 50% discount 10% included tax")
self.assertEqual(round(sol.price_tax), 5.0, "110$ with 50% discount 10% included tax")
so.pricelist_id = pricelist
so.fiscal_position_id = fpos
sol._compute_tax_id()
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=2)
self.assertEqual(round(sol.price_total), 106, "2 units @ 100$ with 50% discount + 6% tax (mapped from fp 10% -> 6%)")
def test_cart_update_with_fpos_no_variant_product(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 for no_variant product
self.env.user.partner_id.write({
'country_id': False,
'property_product_pricelist': self.env.ref('product.list0').id,
})
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', 'type_tax_use': 'sale'})
tax0 = self.env['account.tax'].create({'name': "Test tax 0", 'amount': 0, 'price_include': True, 'amount_type': 'percent', 'type_tax_use': 'sale'})
# 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,
self.env['product.template.attribute.line'].create({
'product_tmpl_id': product_template.id,
'attribute_id': self.ssd_attribute.id,
'value_ids': [Command.set((self.ssd_256.id, self.ssd_512.id))],
})
product = self.env['product.product'].create({
'name': 'prod_no_variant',
'list_price': 110,
'taxes_id': [(6, 0, [tax10.id])],
'type': 'consu',
})
# create an attribute with one variant
product_attribute = self.env['product.attribute'].create({
'name': 'test_attr',
'display_type': 'radio',
'create_variant': 'no_variant',
})
# create attribute value
a1 = self.env['product.attribute.value'].create({
'name': 'pa_value',
'attribute_id': product_attribute.id,
'sequence': 1,
})
# set variant value to product template
product_template = self.env['product.template'].search(
[('name', '=', 'prod_no_variant')], limit=1)
product_template.attribute_line_ids = [(0, 0, {
'attribute_id': product_attribute.id,
'value_ids': [(6, 0, [a1.id])],
})]
# publish the product on website
product_template.is_published = True
# create a so for user using the fiscal position
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
})
sol = self.env['sale.order.line'].create({
'name': product_template.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product_template.uom_id.id,
'price_unit': product_template.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._compute_tax_id()
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=product.id, line_id=sol.id, set_qty=2)
self.assertEqual(round(sol.price_total), 200, "200$ with public price+ 0% tax (mapped from fp 10% -> 0%)")
product_template.product_variant_ids.unlink()
previewed_attribute_values = product_template._get_previewed_attribute_values()
self.assertFalse(previewed_attribute_values)

View file

@ -0,0 +1,488 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.website_sale.controllers.product_configurator import (
WebsiteSaleProductConfiguratorController,
)
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleProductConfigurator(HttpCase, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.pc_controller = WebsiteSaleProductConfiguratorController()
def test_02_variants_modal_window(self):
"""
The objective is to verify that the data concerning the variants are well transmitted
even when passing through a modal window (product configurator).
We create a product with the different attributes and we will modify them.
If the information is not correctly transmitted,
the default values of the variants will be used (the first one).
"""
always_attribute, dynamic_attribute, never_attribute, never_attribute_custom = self.env['product.attribute'].create([
{
'name': 'Always attribute size',
'display_type': 'radio',
'create_variant': 'always'
},
{
'name': 'Dynamic attribute size',
'display_type': 'radio',
'create_variant': 'dynamic'
},
{
'name': 'Never attribute size',
'display_type': 'radio',
'create_variant': 'no_variant'
},
{
'name': 'Never attribute size custom',
'display_type': 'radio',
'create_variant': 'no_variant'
}
])
always_S, always_M, dynamic_S, dynamic_M, never_S, never_M, never_custom_no, never_custom_yes = self.env['product.attribute.value'].create([
{
'name': 'S always',
'attribute_id': always_attribute.id,
},
{
'name': 'M always',
'attribute_id': always_attribute.id,
},
{
'name': 'S dynamic',
'attribute_id': dynamic_attribute.id,
},
{
'name': 'M dynamic',
'attribute_id': dynamic_attribute.id,
},
{
'name': 'S never',
'attribute_id': never_attribute.id,
},
{
'name': 'M never',
'attribute_id': never_attribute.id,
},
{
'name': 'No never custom',
'attribute_id': never_attribute_custom.id,
},
{
'name': 'Yes never custom',
'attribute_id': never_attribute_custom.id,
'is_custom': True,
}
])
product_short = self.env['product.template'].create({
'name': 'Short (TEST)',
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': always_attribute.id,
'value_ids': [(4, always_S.id), (4, always_M.id)],
}),
Command.create({
'attribute_id': dynamic_attribute.id,
'value_ids': [(4, dynamic_S.id), (4, dynamic_M.id)],
}),
Command.create({
'attribute_id': never_attribute.id,
'value_ids': [(4, never_S.id), (4, never_M.id)],
}),
Command.create({
'attribute_id': never_attribute_custom.id,
'value_ids': [(4, never_custom_no.id), (4, never_custom_yes.id)],
}),
]
})
# Add an optional product to trigger the modal window
optional_product = self.env['product.template'].create({
'name': 'Optional product (TEST)',
'website_published': True,
})
product_short.optional_product_ids = [(4, optional_product.id)]
old_sale_order = self.env['sale.order'].search([])
self.start_tour("/", 'tour_variants_modal_window')
# Check the name of the created sale order line
new_sale_order = self.env['sale.order'].search([]) - old_sale_order
new_order_line = new_sale_order.order_line
self.assertEqual(new_order_line.name, 'Short (TEST) (M always, M dynamic)\nNever attribute size: M never\nNever attribute size custom: Yes never custom: TEST')
def test_product_configurator_optional_products(self):
""" Test that the product configurator is shown if the product has optional products. """
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'optional_product_ids': [
Command.create({
'name': "Optional product",
'website_published': True,
})
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertTrue(show_configurator)
def test_optional_products_not_visible_on_other_websites(self):
"""Optional products assigned to a different website should not be shown"""
second_website = self.env['website'].create({'name': 'second website'})
optional_product = self.env['product.template'].create({
'name': "Optional product",
'website_published': True,
'website_id': second_website.id
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'optional_product_ids': [Command.set(optional_product.ids)],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertFalse(show_configurator)
def test_product_configurator_single_variant(self):
""" Test that the product configurator isn't shown if the product has a single variant. """
attribute = self.env['product.attribute'].create({
'name': "Attribute",
'value_ids': [Command.create({'name': "A"})],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set(attribute.value_ids.ids)],
}),
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertFalse(show_configurator)
def test_product_configurator_configured_with_empty_multi_checkbox(self):
""" Test that the product configurator isn't shown if the product is configured and has a
multi-checkbox attribute with no selected values.
"""
multi_attribute = self.env['product.attribute'].create({
'name': "Multi-checkbox attribute",
'display_type': 'multi',
'create_variant': 'no_variant',
'value_ids': [
Command.create({'name': "A"}),
Command.create({'name': "B"}),
],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': multi_attribute.id,
'value_ids': [Command.set(multi_attribute.value_ids.ids)],
}),
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=True
)
self.assertFalse(show_configurator)
def test_product_configurator_only_no_variant_attributes(self):
""" Test that the product configurator is shown if the product isn't configured and has only
no_variant attributes.
"""
no_variant_attribute = self.env['product.attribute'].create({
'name': "No variant attribute",
'create_variant': 'no_variant',
'value_ids': [
Command.create({'name': "A"}),
Command.create({'name': "B"}),
],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': no_variant_attribute.id,
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
}),
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertTrue(show_configurator)
def test_product_configurator_only_dynamic_attributes(self):
""" Test that the product configurator is shown if the product isn't configured and has only
dynamic attributes.
"""
dynamic_attribute = self.env['product.attribute'].create({
'name': "Dynamic attribute",
'create_variant': 'dynamic',
'value_ids': [
Command.create({'name': "A"}),
Command.create({'name': "B"}),
],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': dynamic_attribute.id,
'value_ids': [Command.set(dynamic_attribute.value_ids.ids)],
}),
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertTrue(show_configurator)
def test_product_configurator_single_custom_attribute(self):
""" Test that the product configurator is shown if the product isn't configured and has a
single custom attribute.
"""
custom_attribute = self.env['product.attribute'].create({
'name': "Custom attribute",
'value_ids': [
Command.create({
'name': "Custom value",
'is_custom': True,
}),
],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': custom_attribute.id,
'value_ids': [Command.set(custom_attribute.value_ids.ids)],
}),
],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
self.assertTrue(show_configurator)
def test_product_configurator_sale_not_ok(self):
""" Test that the product configurator skips optional products which aren't `sale_ok`. """
optional_product = self.env['product.template'].create({
'name': "Optional product",
'website_published': True,
'sale_ok': False,
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'optional_product_ids': [Command.set(optional_product.ids)],
})
with MockRequest(self.env, website=self.website):
show_configurator = self.pc_controller.website_sale_should_show_product_configurator(
product_template_id=main_product.id, ptav_ids=[], is_product_configured=False
)
configurator_values = self.pc_controller.website_sale_product_configurator_get_values(
product_template_id=main_product.id,
quantity=1,
currency_id=self.currency.id,
so_date='2000-01-01',
pricelist_id=self.pricelist.id,
)
self.assertFalse(show_configurator)
self.assertListEqual(configurator_values['optional_products'], [])
def test_product_configurator_extra_price_taxes(self):
""" Test that the product configurator applies taxes to PTAV extra prices. """
self.website.show_line_subtotals_tax_selection = 'tax_included'
tax = self.env['account.tax'].create({'name': "Tax", 'amount': 10})
attribute = self.env['product.attribute'].create({
'name': "Attribute",
'value_ids': [Command.create({'name': "A", 'default_extra_price': 1})],
})
product = self.env['product.template'].create({
'name': "Product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set(attribute.value_ids.ids)],
}),
],
'taxes_id': tax,
})
with MockRequest(self.env, website=self.website):
ptav_price_extra = self.pc_controller._get_ptav_price_extra(
product.attribute_line_ids.product_template_value_ids,
self.currency,
datetime(2000, 1, 1),
product,
)
self.assertEqual(ptav_price_extra, 1.1)
def test_product_configurator_zero_priced(self):
""" Test that the product configurator prevents the sale of zero-priced products. """
self.website.prevent_zero_price_sale = True
price_attribute = self.env['product.attribute'].create({
'name': "Price",
'value_ids': [
Command.create({'name': "Zero-priced"}),
Command.create({'name': "Nonzero-priced", 'default_extra_price': 1}),
],
})
optional_product = self.env['product.template'].create({
'name': "Optional product",
'website_published': True,
'list_price': 0,
'attribute_line_ids': [
Command.create({
'attribute_id': price_attribute.id,
'value_ids': [Command.set(price_attribute.value_ids.ids)],
}),
],
})
self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'optional_product_ids': [Command.set(optional_product.ids)],
})
self.start_tour('/', 'website_sale_product_configurator_zero_priced')
def test_product_configurator_strikethrough_price(self):
""" Test that the product configurator displays the strikethrough price correctly. """
self.env['res.config.settings'].create({
'group_product_price_comparison': True,
# Need to enable pricelists for self.pricelist to be considered and applied
'group_product_pricelist': True,
}).execute()
self.website.show_line_subtotals_tax_selection = 'tax_included'
tax = self.env['account.tax'].create({'name': "Tax", 'amount': 10})
optional_product = self.env['product.template'].create({
'name': "Optional product",
'website_published': True,
'list_price': 5,
'compare_list_price': 10,
'taxes_id': tax,
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'list_price': 100,
'optional_product_ids': [Command.set(optional_product.ids)],
'taxes_id': tax,
})
self.pricelist.item_ids = [
Command.create({
'applied_on': '1_product',
'percent_price': 50,
'compute_price': 'percentage',
'product_tmpl_id': main_product.id,
}),
]
self.start_tour('/shop', 'website_sale_product_configurator_strikethrough_price')
def test_get_product_combination_multi_attribute_with_archived_variant_and_inactive_ptav(self):
"""
This test covers a case where a product has multiple attributes and one
of the attribute values corresponds to an archived variant, with its
ptav_active set to False.
In this scenario, a valid combination should still be possible, and the
resulting combination product must not be the archived variant.
"""
attribute_single = self.env['product.attribute'].create({
'name': "attribute single",
'value_ids': [
Command.create({
'name': "single",
}),
],
})
attribute_multi = self.env['product.attribute'].create({
'name': "attribute multi",
'value_ids': [
Command.create({'name': "first"}),
Command.create({'name': "second"}),
Command.create({'name': "third"}),
],
})
main_product = self.env['product.template'].create({
'name': "Main product",
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute_single.id,
'value_ids': [Command.set(attribute_single.value_ids.ids)],
}),
Command.create({
'attribute_id': attribute_multi.id,
'value_ids': [Command.set(attribute_multi.value_ids.ids)],
}),
],
})
main_product.product_variant_ids.filtered(
lambda product: product.product_template_attribute_value_ids[1].name == 'first',
).action_archive()
main_product.attribute_line_ids[1].product_template_value_ids[0].ptav_active = False
with MockRequest(self.env, website=self.website):
product_values = self.pc_controller._prepare_product_values(
main_product,
self.env['product.public.category'],
attribute_values=str(attribute_single.value_ids.id),
)
is_combination_possible = product_values['combination_info']['is_combination_possible']
combination_product_id = product_values['combination_info']['product_id']
self.assertTrue(is_combination_possible)
self.assertTrue(self.env['product.product'].browse(combination_product_id).active)

View file

@ -0,0 +1,333 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import tagged, HttpCase
from odoo.tools import SQL
from odoo.addons.product.tests.test_product_attribute_value_config import (
TestProductAttributeValueCommon,
)
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleProductFilters(WebsiteSaleCommon, TestProductAttributeValueCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.WebsiteSnippetFilter = cls.env['website.snippet.filter'].with_context({
'limit': 16,
'search_domain': [],
'allowed_company_ids': cls.env.company.ids,
})
# Computer accessories
cls.color_attribute = cls.env['product.attribute'].create({
'name': "Color",
'value_ids': [
Command.create({'name': name})
for name in ("Black", "Grey", "White", "Beige", "Red", "Pink")
],
})
cls.env['product.template.attribute.line'].create({
'product_tmpl_id': cls.computer_case.id,
'attribute_id': cls.color_attribute.id,
'value_ids': [Command.set(cls.color_attribute.value_ids.ids)],
})
cls.computer_case.website_published = True
cls.black_case_M = cls.computer_case.product_variant_ids.filtered_domain([
('product_template_attribute_value_ids.name', 'in', "Black"),
('product_template_attribute_value_ids.name', 'in', "M"),
])
cls.pink_case_M = cls.computer_case.product_variant_ids.filtered_domain([
('product_template_attribute_value_ids.name', 'in', "Pink"),
('product_template_attribute_value_ids.name', 'in', "M"),
])
cls.pink_case_L = cls.computer_case.product_variant_ids.filtered_domain([
('product_template_attribute_value_ids.name', 'in', "Pink"),
('product_template_attribute_value_ids.name', 'in', "L"),
])
cls.monitor = cls.env['product.template'].create({
'name': "Super Computer Monitor",
'list_price': 200,
'website_published': True,
})
cls.accessories = cls.computer_case.product_variant_ids + cls.monitor.product_variant_id
cls.computer.write({
'company_id': False,
'website_published': True,
'accessory_product_ids': cls.accessories.ids,
})
# Computer alternatives
cls.windows_pc = cls._create_product(
name='Windows PC',
lst_price=1000.0,
standard_price=800.0,
alternative_product_ids=[Command.set(cls.computer.ids)],
).product_tmpl_id
cls.mac = cls._create_product(
name='Mac',
uom_id=cls.uom_dozen.id,
lst_price=200.0,
standard_price=160.0,
alternative_product_ids=[Command.link(cls.computer.id), Command.link(cls.windows_pc.id)]
).product_tmpl_id
# More generic products to get the number of product templates to 17
generics = cls.env['product.template'].create([{
'name': f"Generic product {i}",
'company_id': False,
'website_published': True,
} for i in range(1, 13)])
cls.product_tmpls = (
cls.computer_case + cls.monitor + cls.computer + cls.windows_pc + cls.mac + generics
)
# Archive all products not relevant to the test suite, bypassing ORM constraints
cls.env.invalidate_all()
cls.env.cr.execute(SQL('; ').join(
SQL(
'UPDATE %s SET active = false WHERE id NOT IN %s',
SQL.identifier(recs._table), recs._ids,
) for recs in (cls.product_tmpls.product_variant_ids, cls.product_tmpls)
))
def test_latest_sold_filter(self):
"""Check the latest sold filter after selling 1 computer and 3 different cases.
When showing variants, the computer should be the most sold product.
When hiding variants, the case should be the most sold product.
"""
computer = self.computer.product_variant_id
self.empty_cart.write({
'website_id': self.website.id,
'order_line': [
Command.create({'product_id': product_id})
for product_id in (computer + self.pink_case_M + self.pink_case_L).ids
],
})
self.empty_cart.action_confirm()
self.cart.order_line.unlink()
self.cart.write({
'website_id': self.website.id,
'order_line': [
Command.create({'product_id': product_id})
for product_id in (computer + self.black_case_M).ids
],
})
self.cart.action_confirm()
dyn_filter = self.env.ref('website_sale.dynamic_filter_latest_sold_products')
with MockRequest(self.env, website=self.website):
with_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=False,
website_id=self.website.id,
)._get_products('latest_sold')
self.assertSetEqual(
{p['product_id'] for p in with_variants},
{computer.id, *(self.pink_case_L + self.pink_case_M + self.black_case_M).ids},
'"Latest sold" filter should return 4 products without hiding variants',
)
self.assertEqual(
with_variants[0]['product_id'],
computer.id,
"When showing variants, `computer` should be the most sold product",
)
no_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=True,
website_id=self.website.id,
)._get_products('latest_sold')
self.assertSetEqual(
{p['product_id'] for p in no_variants},
{computer.id, self.computer_case.product_variant_id.id},
'"Latest sold" filter should return 2 products when hiding variants',
)
self.assertEqual(
no_variants[0]['product_id'],
self.computer_case.product_variant_id.id,
"When hiding variants, `computer_case` should be the most sold product",
)
def test_latest_viewed_filter(self):
"""Check the latest viewed filter after viewing 2 different cases and 1 computer.
When showing variants, the filter should return 3 items.
When hiding variants, the filter should return 2 items.
"""
viewed_products = self.black_case_M + self.pink_case_L + self.computer.product_variant_id
dyn_filter = self.env.ref('website_sale.dynamic_filter_latest_viewed_products')
with MockRequest(self.env, website=self.website):
visitor = self.env['website.visitor']._upsert_visitor(self.env.user.partner_id.id)
self.env['website.track'].create([{
'visitor_id': visitor[0],
'product_id': product_id,
} for product_id in viewed_products.ids])
with_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=False,
)._get_products('latest_viewed')
self.assertSetEqual(
{p['product_id'] for p in with_variants},
set(viewed_products.ids),
'When showing variants, "Latest viewed" filter should return viewed variants',
)
no_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=True,
)._get_products('latest_viewed')
self.assertSetEqual(
{p['product_id'] for p in no_variants},
{self.computer_case.product_variant_id.id, self.computer.product_variant_id.id},
'When hiding variants, "Latest viewed" filter should return 1 variant per template',
)
def test_recently_sold_with_filter(self):
"""Check the recently-sold-with filter after selling 1 computer, 1 monitor & 1 case.
When showing variants, the filter should return the sold variants.
When hiding variants, the filter should return the default variants.
"""
computer = self.computer.product_variant_id
monitor = self.monitor.product_variant_id
self.empty_cart.write({
'website_id': self.website.id,
'order_line': [
Command.create({'product_id': product_id})
for product_id in (computer + monitor + self.pink_case_L).ids
],
})
self.empty_cart.action_confirm()
dyn_filter = self.env.ref('website_sale.dynamic_filter_cross_selling_recently_sold_with')
with MockRequest(self.env, website=self.website):
with_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=False,
website_id=self.website.id,
)._get_products('recently_sold_with', product_template_id=str(self.computer.id))
self.assertSetEqual(
{p['product_id'] for p in with_variants},
{self.monitor.product_variant_id.id, self.pink_case_L.id},
'"Recently sold with" filter should return sold variants when showing variants',
)
no_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=True,
website_id=self.website.id,
)._get_products('recently_sold_with', product_template_id=str(self.computer.id))
self.assertSetEqual(
{p['product_id'] for p in no_variants},
{self.monitor.product_variant_id.id, self.computer_case.product_variant_id.id},
'"Recently sold with" filter should return generic variants when hiding variants',
)
def test_accessories_filter(self):
"""Check the accessories filter on the computer product.
When showing variants, the filter should return 16 (limit) accessory products.
When hiding variants, the filter should return 2 products: monitor & case.
"""
dyn_filter = self.env.ref('website_sale.dynamic_filter_cross_selling_accessories')
with MockRequest(self.env, website=self.website):
with_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=False,
)._get_products('accessories', product_template_id=str(self.computer.id))
self.assertListEqual(
[p['product_id'] for p in with_variants],
self.computer_case.product_variant_ids.ids[:16],
"Accessories filter should return 16 results when showing variants",
)
no_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=True,
)._get_products('accessories', product_template_id=str(self.computer.id))
self.assertListEqual(
[p['product_id'] for p in no_variants],
self.accessories.product_variant_id.ids,
"Accessories filter should return 2 results when hiding variants",
)
def test_alternative_products_filter(self):
"""Check the alternative products filter on the Mac product.
When showing variants, the filter should return 16 (limit) alternative products.
When hiding variants, the filter should return 2 products: computer & Windows PC.
"""
dyn_filter = self.env.ref('website_sale.dynamic_filter_cross_selling_alternative_products')
with MockRequest(self.env, website=self.website):
with_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=False,
)._get_products('alternative_products', product_template_id=str(self.mac.id))
self.assertListEqual(
[p['product_id'] for p in with_variants],
self.mac.alternative_product_ids.product_variant_ids.ids[:16],
"Alternative products filter should return 16 results when showing variants",
)
no_variants = self.WebsiteSnippetFilter.with_context(
dynamic_filter=dyn_filter,
hide_variants=True,
)._get_products('alternative_products', product_template_id=str(self.mac.id))
self.assertListEqual(
[p['product_id'] for p in no_variants],
[self.computer.product_variant_id.id, self.windows_pc.product_variant_id.id],
"Alternative products filter should return 2 results when hiding variants",
)
def test_newest_products_filter(self):
"""Check the newest products filter.
When showing variants, the filter should return 16 variants with repeating templates.
When hiding variants, the filter should return 16 templates, all unique.
This filter is unique in that it's defined in `data/data.xml`, and hence can't be called
via the `_get_products` method.
"""
# Ensure we're working with a known set of products
self.env['product.template'].search([('id', 'not in', self.product_tmpls.ids)]).write({
'sale_ok': False,
})
dyn_filter = self.env.ref('website_sale.dynamic_filter_newest_products')
with MockRequest(self.env, website=self.website):
with_variants = dyn_filter._prepare_values(search_domain=[])
self.assertEqual(
len(with_variants),
16,
"When displaying newest variants, 16 records should be shown",
)
self.assertLess(
len({p['product_template_id'] for p in with_variants}),
16,
"When displaying newest variants, some product templates should be repeating",
)
no_variants = dyn_filter._prepare_values(search_domain=['hide_variants'])
self.assertEqual(len(no_variants), 16)
self.assertEqual(
len({p['product_template_id'] for p in no_variants}),
16,
"When displaying newest product templates, 16 unique templates should be shown",
)
def test_shop_attribute_filters_remain_when_changing_page(self):
self.env['product.attribute'].search([]).write({'visibility': 'hidden'})
self.color_attribute.visibility = 'visible'
self.size_attribute.visibility = 'visible'
self.env['website'].get_current_website().shop_ppg = 1
computer_case_copy = self.computer_case.copy()
computer_case_copy.website_published = True
self.start_tour('/shop', 'shop_attribute_filters_remain_when_changing_page')

View file

@ -0,0 +1,101 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleProductPage(HttpCase, ProductVariantsCommon, WebsiteSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_template_sofa.website_published = True
def test_toggle_contact_us_button_visibility(self):
"""Check that the "Contact Us" button:
- is shown for zero-priced products
- is hidden for other products
- is not displayed at the same time as the "Add to Cart" button
"""
self.website.prevent_zero_price_sale = True
self.product_template_sofa.list_price = 0
red_sofa = self.product_template_sofa.product_variant_ids[:1]
red_sofa.product_template_attribute_value_ids.price_extra = 20
self.start_tour(red_sofa.website_url, 'website_sale_contact_us_button')
def test_product_reviews_reactions_public(self):
""" Check that public users can not react to reviews """
password = "Pl1bhD@2!kXZ"
manager = self.env.ref("base.user_admin")
manager.write({"password": password})
self.env["ir.ui.view"].with_context(active_test=False).search([
("key", "=", "website_sale.product_comment")
]).write({"active": True})
self.product_product_7 = self.env["product.product"].create({
"name": "Storage Box Test",
"standard_price": 70.0,
"list_price": 79.0,
"website_published": True,
"invoice_policy": "delivery",
})
message = self.product_product_7.product_tmpl_id.message_post(
body="Bad box!",
message_type="comment",
rating_value="1",
subtype_xmlid="mail.mt_comment"
)
self.authenticate(manager.login, password)
self.make_jsonrpc_request(
"/mail/message/reaction",
{
"action": "add",
"content": "😊",
"message_id": message.id,
},
)
self.start_tour("/", 'website_sale_product_reviews_reactions_public', login=None)
def test_product_pricelist_qty_change(self):
"""Check that pricelist discounts based on product quantity display when applicable."""
self.env['res.config.settings'].create({'group_product_pricelist': True}).execute()
self.pricelist.item_ids = [
Command.clear(),
Command.create({
'categ_id': self.product_category.id,
'compute_price': 'percentage',
'min_quantity': 5.0,
'percent_price': 50.0,
}),
]
self.start_tour(self.product.website_url, 'website_sale_product_pricelist_qty_change')
def test_product_unpublished_without_category(self):
"""Test that products created from frontend are unpublished without category"""
self.start_tour("/", 'product_unpublished_without_category', login="admin")
product = self.env['product.product'].search(
[('name', '=', 'Product Without Category')],
limit=1,
)
self.assertTrue(product)
self.assertFalse(product.website_published)
def test_product_published_with_category(self):
"""Test that products with category are published"""
self.env['product.public.category'].create({'name': 'Test Category'})
self.start_tour("/", 'product_published_with_category', login="admin")
product = self.env['product.product'].search(
[('name', '=', 'Product With Category')],
limit=1,
)
self.assertTrue(product)
self.assertTrue(product.website_published)

View file

@ -0,0 +1,93 @@
from datetime import timedelta
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
class TestProductRibbon(WebsiteSaleCommon):
def setUp(self):
super().setUp()
# Manual ribbon
self.manual_ribbon = self.env['product.ribbon'].create({
'name': "Manual Ribbon",
'assign': 'manual',
})
# Sale ribbon
self.sale_ribbon = self.env['product.ribbon'].create({
'name': "Sale Ribbon",
'assign': 'sale',
})
# New ribbon
self.new_ribbon = self.env['product.ribbon'].create({
'name': "New Ribbon",
'assign': 'new',
})
self.auto_assign_ribbon = self.env['product.ribbon'].search([('assign', '!=', 'manual')])
def test_manual_ribbon_assignment(self):
self.product.website_ribbon_id = self.manual_ribbon.id
products_prices = {'base_price': 100, 'price_reduce': 100}
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon, self.manual_ribbon, "Manual ribbon should be returned",
)
def test_sale_ribbon_assignment(self):
self.product.list_price = 100
products_prices = {'base_price': 100, 'price_reduce': 80} # discounted
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon, self.sale_ribbon, "Sale ribbon should be returned",
)
def test_new_ribbon_assignment(self):
self.product.publish_date -= timedelta(days=10)
products_prices = {'base_price': 100, 'price_reduce': 100}
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon,
self.new_ribbon,
"New ribbon should be returned for recently published products",
)
def test_no_ribbon_if_none_match(self):
self.product.publish_date -= timedelta(days=100)
products_prices = {'base_price': 100, 'price_reduce': 100}
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon, self.product)
self.assertFalse(
ribbon, "No ribbon should be returned when no condition is matched",
)
def test_ribbon_priority_assignment(self):
self.product.website_ribbon_id = self.manual_ribbon.id
self.product.publish_date -= timedelta(days=10)
products_prices = {'base_price': 100, 'price_reduce': 80} # discounted
self.sale_ribbon.sequence = 1
self.new_ribbon.sequence = 2
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon,
self.manual_ribbon,
"Manual ribbon should have the highest priority",
)
self.product.website_ribbon_id = False
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon,
self.sale_ribbon,
"Sale ribbon should have the highest priority",
)
self.new_ribbon.sequence = 1
self.sale_ribbon.sequence = 2
self.auto_assign_ribbon = self.env['product.ribbon'].search([('assign', '!=', 'manual')])
ribbon = self.product.product_tmpl_id._get_ribbon(products_prices, self.auto_assign_ribbon)
self.assertEqual(
ribbon,
self.new_ribbon,
"New ribbon should have the highest priority",
)

View file

@ -0,0 +1,135 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo.fields import Command, Date
from odoo.tests import tagged
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleProductTemplate(WebsiteSaleCommon):
def test_website_sale_get_configurator_display_price(self):
self.website.show_line_subtotals_tax_selection = 'tax_included'
tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 10})
product = self._create_product(list_price=100, taxes_id=[Command.link(tax.id)])
env = self.env(user=self.public_user)
with MockRequest(env, website=self.website.with_env(env)):
configurator_price = self.env['product.template']._get_configurator_display_price(
product_or_template=product,
quantity=3,
date=datetime(2000, 1, 1),
currency=self.currency,
pricelist=self.pricelist,
)
self.assertEqual(configurator_price[0], 110)
def test_markup_data_uses_group_schema_when_multiple_variants(self):
product_attribute = self.env['product.attribute'].create({
'name': 'Test attribute',
'create_variant': 'always',
'value_ids': [
Command.create({'name': 'Test value 1'}),
Command.create({'name': 'Test value 2'}),
],
})
product_template = self.env['product.template'].create({
'name': 'Test product',
'attribute_line_ids': [
Command.create({
'attribute_id': product_attribute.id,
'value_ids': [Command.set(product_attribute.value_ids.ids)],
}),
],
})
website = self.website
with MockRequest(website.env, website=website):
markup_data = product_template._to_markup_data(self.website)
self.assertEqual(markup_data['@type'], 'ProductGroup')
self.assertEqual(len(markup_data['hasVariant']), 2)
def test_markup_data_uses_product_schema_when_single_variant(self):
product_template = self.env['product.template'].create({'name': 'Test product'})
website = self.website
with MockRequest(website.env, website=website):
markup_data = product_template._to_markup_data(self.website)
self.assertEqual(markup_data['@type'], 'Product')
def test_markup_data_uses_taxes_excluded_price_when_configured_on_website(self):
self.env['res.config.settings'].create({
'show_line_subtotals_tax_selection': 'tax_excluded'
}).execute()
with MockRequest(self.website.env, website=self.website):
markup_data = self.product._to_markup_data(self.website)
self.assertEqual(
markup_data['offers']['price'],
self.website.currency_id.round(self.product.base_unit_price),
)
def test_markup_data_uses_taxes_included_price_when_configured_on_website(self):
self.env['res.config.settings'].create({
'show_line_subtotals_tax_selection': 'tax_included'
}).execute()
self.product.price_extra = 10
with MockRequest(self.website.env, website=self.website):
product_tmpl = self.product.product_tmpl_id
markup_data = self.product._to_markup_data(self.website)
self.assertEqual(
markup_data['offers']['price'],
self.website.currency_id.round(
self.product.base_unit_price * (1 + product_tmpl.taxes_id[0].amount / 100)
),
)
def test_markup_data_converts_price_to_website_currency(self):
company_currency = self.env.company.currency_id
# Find a currency different from the company currency.
self.website.currency_id = self.env['res.currency'].with_context(active_test=False).search([
('name', '!=', company_currency.name)
], limit=1)
with MockRequest(self.env, website=self.website):
markup = self.product._to_markup_data(self.website)
# Expected converted price
expected_price = company_currency._convert(
self.product.list_price,
self.website.currency_id,
company=self.env.company,
date=Date.from_string('2020-01-01'),
)
self.assertAlmostEqual(markup['offers']['price'], expected_price, places=2)
def test_remove_archived_products_from_cart(self):
"""Archived products shouldn't appear in carts"""
self.product.action_archive()
self.assertNotIn(
self.product, self.cart.order_line.product_id,
"Archived product should be deleted from the cart.",
)
self.service_product.product_tmpl_id.action_archive()
self.assertNotIn(
self.service_product, self.cart.order_line.product_id,
"All products from archived product templates should be removed from the cart.",
)
def test_get_additionnal_combination_info_converts_price_to_website_currency(self):
company_currency = self.env.company.currency_id
# Find a currency different from the company currency.
self.website.currency_id = self.env['res.currency'].with_context(active_test=False).search([
('name', '!=', company_currency.name)
], limit=1)
with MockRequest(self.env, website=self.website):
result = self.env['product.template']._get_additionnal_combination_info(
self.product, 1.0, self.product.uom_id, Date.from_string('2020-01-01'), self.website
)
# Expected converted price
expected_price = company_currency._convert(
self.product.list_price,
self.website.currency_id,
company=self.env.company,
date=Date.from_string('2020-01-01'),
)
self.assertAlmostEqual(result['price'], expected_price, places=2)

View file

@ -1,19 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.http_routing.tests.common import MockRequest
@tagged('post_install', '-at_install')
class TestWebsiteSaleReorderFromPortal(HttpCase):
class TestWebsiteSaleReorderFromPortal(HttpCaseWithUserPortal):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['website'].get_current_website().enabled_portal_reorder_button = True
def test_website_sale_reorder_from_portal(self):
product_1, product_2 = self.env['product.product'].create([
cls.website = cls.env['website'].get_current_website()
cls.website.write({
'prevent_zero_price_sale': False,
})
cls.empty_cart = cls.env['sale.order'].create({
'partner_id': cls.partner_portal.id,
})
cls.product_1, cls.product_2 = cls.env['product.product'].create([
{
'name': 'Reorder Product 1',
'sale_ok': True,
@ -25,6 +33,8 @@ class TestWebsiteSaleReorderFromPortal(HttpCase):
'website_published': True,
},
])
def test_website_sale_reorder_from_portal(self):
no_variant_attribute = self.env['product.attribute'].create({
'name': 'Size',
'create_variant': 'no_variant',
@ -46,15 +56,15 @@ class TestWebsiteSaleReorderFromPortal(HttpCase):
ptav_s = ptavs.filtered(lambda ptav: ptav.product_attribute_value_id == s)
ptav_l = ptavs.filtered(lambda ptav: ptav.product_attribute_value_id == l)
user_admin = self.env.ref('base.user_admin')
order = self.env['sale.order'].create({
order = self.empty_cart
order.write({
'partner_id': user_admin.partner_id.id,
'state': 'sale',
'order_line': [
Command.create({
'product_id': product_1.id,
'product_id': self.product_1.id,
}),
Command.create({
'product_id': product_2.id,
'product_id': self.product_2.id,
}),
Command.create({
'product_id': no_variant_template.product_variant_id.id,
@ -70,6 +80,7 @@ class TestWebsiteSaleReorderFromPortal(HttpCase):
})
],
})
order.action_confirm()
order.message_subscribe(user_admin.partner_id.ids)
self.start_tour("/", 'website_sale_reorder_from_portal', login='admin')
@ -79,8 +90,69 @@ class TestWebsiteSaleReorderFromPortal(HttpCase):
order_lines = reorder_cart.order_line
self.assertEqual(previous_lines.product_id, order_lines.product_id)
self.assertEqual(previous_lines.mapped('name'), order_lines.mapped('name'))
self.assertEqual(
previous_lines.product_id.sorted('id').mapped('name'),
order_lines.product_id.sorted('id').mapped('name'),
)
self.assertEqual(
previous_lines.product_no_variant_attribute_value_ids,
order_lines.product_no_variant_attribute_value_ids,
)
def test_is_reorder_allowed(self):
line_published_product = Command.create({
'product_id': self.product_1.id,
})
self.product_2.active = False
line_archived_product = Command.create({
'product_id': self.product_2.id,
})
line_section = Command.create({
'name': "Free line",
'display_type': 'line_section',
})
line_downpayment = Command.create({
'name': "Down with the Payment",
'is_downpayment': True,
'price_unit': 5,
})
order = self.empty_cart.with_user(self.user_portal).sudo()
order.order_line = [line_section]
order.action_confirm()
with MockRequest(self.env, website=self.website):
self.assertFalse(
order._is_reorder_allowed(),
"Reordering a line section should not be allowed",
)
order.order_line = [line_archived_product]
self.assertFalse(
order._is_reorder_allowed(),
"Reordering an archived product should not be allowed",
)
order.order_line = [line_published_product]
self.assertTrue(
order._is_reorder_allowed(),
"Reordering a published product should be allowed",
)
self.product_1.website_published = False
self.assertFalse(
order._is_reorder_allowed(),
"Reordering an unpublished product should not be allowed",
)
order.order_line = [line_downpayment]
self.assertFalse(
order._is_reorder_allowed(),
"Reordering a down payment should not be allowed",
)
self.product_2.write({'active': True, 'list_price': 0.0})
self.assertTrue(
order._is_reorder_allowed(),
"Reordering a zero-priced product should be allowed when enabled",
)

View file

@ -0,0 +1,24 @@
from odoo.fields import Command
from odoo.tests import HttpCase
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
class WebsiteSaleSEO(HttpCase, WebsiteSaleCommon):
def test_website_sale_user_designer_can_edit_seo(self):
public_categ = self.env['product.public.category'].create({'name': 'Website Category'})
self.product.write({'public_categ_ids': [Command.link(public_categ.id)]})
internal_user = self.env['res.users'].create({
'name': 'Web Designer',
'login': 'internal_user',
'group_ids': [
Command.link(self.ref('website.group_website_designer')),
Command.link(self.ref('base.group_user')),
],
})
self.authenticate(internal_user.login, internal_user.login)
res = self.make_jsonrpc_request(
'/website/get_seo_data',
{'res_id': public_categ.id, 'res_model': 'product.public.category'},
)
self.assertTrue(res['can_edit_seo'])

View file

@ -0,0 +1,33 @@
from odoo.tests import tagged
from odoo.addons.base.tests.common import BaseCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleSettings(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env['res.company'].create({
'name': 'Test Company',
})
cls.website = cls.env['website'].create({
'name': 'Test Website',
'company_id': cls.company.id,
'account_on_checkout': 'mandatory',
'auth_signup_uninvited': 'b2b',
})
def test_settings_account_on_checkout(self):
# only change auth_signup_uninvited if account_on_checkout was changed
config = self.env['res.config.settings'].with_company(self.company)
config.create({'account_on_checkout': 'mandatory'}).execute()
self.assertEqual(self.website.auth_signup_uninvited, 'b2b')
config.create({'account_on_checkout': 'optional'}).execute()
self.assertEqual(self.website.auth_signup_uninvited, 'b2c')
config.create({'account_on_checkout': 'disabled'}).execute()
self.assertEqual(self.website.auth_signup_uninvited, 'b2b')
config.create({'auth_signup_uninvited': 'b2c', 'account_on_checkout': 'disabled'}).execute()
self.assertEqual(self.website.auth_signup_uninvited, 'b2c')
config.create({'auth_signup_uninvited': 'b2b', 'account_on_checkout': 'mandatory'}).execute()
self.assertEqual(self.website.auth_signup_uninvited, 'b2c')

View file

@ -0,0 +1,141 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import HttpCase, tagged
from odoo.addons.website_sale.controllers.main import SHOP_PATH
from odoo.addons.website_sale.tests.common import WebsiteSaleCommon
@tagged('post_install', '-at_install')
class TestWebsiteSaleShopRedirects(HttpCase, WebsiteSaleCommon):
def test_website_sale_shop_redirects(self):
category_a = self.env['product.public.category'].create({'name': "Category A"})
category_b = self.env['product.public.category'].create({'name': "Category B"})
test_product = self.env['product.template'].create({
'name': "Test product",
'public_categ_ids': [Command.link(category_a.id)],
'website_published': True,
})
# Add a different published product to category B so that it is accessible to public users
self._create_product(website_published=True, public_categ_ids=[Command.link(category_b.id)])
slug = self.env['ir.http']._slug
response = self.url_open(
f'/shop?category={category_a.id}&some-key=some-value',
allow_redirects=False,
)
self.assertEqual(response.status_code, 301)
self.assertURLEqual(
response.headers.get('Location'),
f'/shop/category/{slug(category_a)}?some-key=some-value',
)
response = self.url_open(
f'/shop/product/{slug(test_product)}?category={category_a.id}&some-key=some-value',
allow_redirects=False,
)
self.assertEqual(response.status_code, 301)
self.assertURLEqual(
response.headers.get('Location'),
f'/shop/{slug(category_a)}/{slug(test_product)}?some-key=some-value',
)
response = self.url_open(
f'/shop/product/{slug(test_product)}?category=test&some-key=some-value',
allow_redirects=False,
)
self.assertEqual(response.status_code, 301)
self.assertURLEqual(
response.headers.get('Location'),
f'/shop/{slug(test_product)}?some-key=some-value'
)
response = self.url_open(
f'/shop/{slug(category_b)}/{slug(test_product)}?some-key=some-value',
allow_redirects=False,
)
self.assertEqual(response.status_code, 301)
self.assertURLEqual(
response.headers.get('Location'),
f'/shop/{slug(test_product)}?some-key=some-value',
)
def test_ecommerce_product_page_url_unpublished_product(self):
# Unpublished products should be hidden and return a 404.
accessory_product = self.env['product.template'].create({
'name': 'Access Product',
'is_published': False,
})
url = f'{SHOP_PATH}/{accessory_product.id}'
res = self.url_open(url)
self.assertEqual(len(res.history), 0, "Unpublished products shouldn't redirect.")
self.assertURLEqual(res.url, url, "Unpublished products shouldn't slug the URL.")
self.assertEqual(res.status_code, 404, "Unpublished products should return a 404 page.")
def test_ecommerce_category_page_url_invalid_category(self):
# Invalid category should return a 404.
url = f'{SHOP_PATH}/category/999999'
res = self.url_open(url)
self.assertEqual(res.status_code, 404, "Invalid category should return a 404.")
def test_ecommerce_product_page_url_invalid_category(self):
# Invalid category should redirect to the canonical product page.
accessory_product = self.env['product.template'].create({
'name': 'Access Product',
'is_published': True,
})
url = f'{SHOP_PATH}/999999/{accessory_product.id}'
res = self.url_open(url)
self.assertEqual(
len(res.history),
1,
"Invalid category with valid product should only redirect once to the product page.",
)
self.assertEqual(
res.history[0].status_code,
303,
"Invalid category with valid product should redirect to the product page.",
)
good_url = f'{SHOP_PATH}/{self.env["ir.http"]._slug(accessory_product)}'
self.assertURLEqual(res.url, good_url)
def test_ecommerce_category_page_url_unpublished_product(self):
# Unpublished product should redirect to the canonical category page (if category provided).
category = self.env['product.public.category'].create({
'name': 'Test Category',
})
# Add a different published product to category so that it is accessible to public users
self.env['product.template'].create({
'name': 'Test Product',
'is_published': True,
'public_categ_ids': [
Command.link(category.id),
],
})
accessory_product = self.env['product.template'].create({
'name': 'Access Product',
'public_categ_ids': [
Command.link(category.id),
],
})
accessory_product.is_published = False
url = f'{SHOP_PATH}/{category.id}/{accessory_product.id}'
res = self.url_open(url)
self.assertEqual(
len(res.history),
1,
"Unpublished product with valid category should only redirect once to the category page.",
)
self.assertEqual(
res.history[0].status_code,
303,
"Unpublished product with valid category should redirect to the category page.",
)
good_url = f'{SHOP_PATH}/category/{self.env["ir.http"]._slug(category)}'
self.assertURLEqual(res.url, good_url)

View file

@ -10,16 +10,12 @@ from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls._enable_pricelists()
ProductTemplate = cls.env['product.template']
Pricelist = cls.env['product.pricelist']
PricelistItem = cls.env['product.pricelist.item']
Currency = cls.env['res.currency']
CurrencyRate = cls.env['res.currency.rate']
# Cleanup existing pricelist.
cls.env['website'].search([]).write({'sequence': 1000})
website = cls.env['website'].create({
'name': "Test website",
@ -29,14 +25,12 @@ class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHt
cls.test_product_default = ProductTemplate.create({
'name': 'test_product_default',
'type': 'consu',
'website_published': True,
'list_price': 1000,
'company_id': cls.env.company.id,
})
cls.test_product_with_compare_list_price = ProductTemplate.create({
'name': 'test_product_with_compare_list_price',
'type': 'consu',
'website_published': True,
'list_price': 2000,
'compare_list_price': 2500,
@ -45,30 +39,17 @@ class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHt
cls.test_product_with_pricelist = ProductTemplate.create({
'name': 'test_product_with_pricelist',
'website_published': True,
'type': 'consu',
'list_price': 2000,
'company_id': cls.env.company.id,
})
cls.test_product_with_pricelist_and_compare_list_price = ProductTemplate.create({
'name': 'test_product_with_pricelist_and_compare_list_price',
'website_published': True,
'type': 'consu',
'list_price': 4000,
'compare_list_price': 4500,
'company_id': cls.env.company.id,
})
cls.test_custom_currency = Currency.create({
'name': "Test currency",
'symbol': 'A',
})
CurrencyRate.create({
'currency_id': cls.test_custom_currency.id,
'name': '2000-01-01',
'rate': 2.0,
})
# Three pricelists
Pricelist.search([]).write({'sequence': 1000})
cls.pricelist_default = Pricelist.create({
@ -84,7 +65,20 @@ class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHt
'company_id': cls.env.company.id,
'selectable': True,
'sequence': 2,
'discount_policy': 'with_discount',
'item_ids': [
Command.create({
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist.id,
'compute_price': 'fixed',
'fixed_price': 1500,
}),
Command.create({
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist_and_compare_list_price.id,
'compute_price': 'percentage',
'percent_price': 12.5,
})
]
})
cls.pricelist_without_discount = Pricelist.create({
'name': 'pricelist_without_discount',
@ -92,54 +86,25 @@ class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHt
'company_id': cls.env.company.id,
'selectable': True,
'sequence': 3,
'discount_policy': 'without_discount',
})
cls.pricelist_other_currency = Pricelist.create({
'name': 'pricelist_other_currency',
'website_id': website.id,
'company_id': cls.env.company.id,
'selectable': True,
'sequence': 4,
'currency_id': cls.test_custom_currency.id,
})
# Pricelist items
PricelistItem.create({
'pricelist_id': cls.pricelist_with_discount.id,
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist.id,
'compute_price': 'fixed',
'fixed_price': 1500,
})
PricelistItem.create({
'pricelist_id': cls.pricelist_without_discount.id,
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist.id,
'compute_price': 'fixed',
'fixed_price': 1500,
})
PricelistItem.create({
'pricelist_id': cls.pricelist_without_discount.id,
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist_and_compare_list_price.id,
'compute_price': 'fixed',
'fixed_price': 3500,
})
PricelistItem.create({
'pricelist_id': cls.pricelist_with_discount.id,
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist_and_compare_list_price.id,
'compute_price': 'fixed',
'fixed_price': 3500,
'item_ids': [
Command.create({
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist.id,
'compute_price': 'percentage',
'percent_price': 25,
}),
Command.create({
'applied_on': '1_product',
'product_tmpl_id': cls.test_product_with_pricelist_and_compare_list_price.id,
'compute_price': 'percentage',
'percent_price': 12.5,
})
]
})
def test_compare_list_price_price_list_display(self):
self.env.user.write({
'groups_id': [Command.link(
self.env.ref('website_sale.group_product_price_comparison').id
)],
})
self.env['res.config.settings'].create({
'group_product_pricelist': True,
'group_product_price_comparison': True,
}).execute()
self.start_tour("/", 'compare_list_price_price_list_display', login=self.env.user.login)

View file

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo.tests import HttpCase, tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.http_routing.tests.common import MockRequest
_logger = logging.getLogger(__name__)
@ -40,20 +41,46 @@ class TestSnippets(HttpCase):
self.start_tour('/', 'website_sale.snippet_products', login='admin')
def test_02_snippet_products_remove(self):
self.user = self.env['res.users'].search([('login', '=', 'admin')])
self.website_visitor = self.env['website.visitor'].search([('partner_id', '=', self.user.partner_id.id)])
before_tour_product_ids = self.website_visitor.product_ids.ids
with MockRequest(self.env, website=self.env['website'].get_current_website()):
if not self.website_visitor:
self.website_visitor = self.env['website.visitor'].create({'partner_id': self.user.partner_id.id})
self.product = self.env['product.product'].create({
'name': 'Storage Box',
'website_published': True,
'image_512': b'/product/static/img/product_product_9-image.jpg',
'display_name': 'Bin',
'description_sale': 'Pedal-based opening system',
})
self.website_visitor._add_viewed_product(self.product.id)
self.start_tour('/', 'website_sale.products_snippet_recently_viewed', login='admin')
after_tour_product_ids = self.website_visitor.product_ids.ids
self.assertEqual(before_tour_product_ids, after_tour_product_ids, "There shouldn't be any new product in recently viewed after this tour")
Visitor = self.env['website.visitor']
user = self.env['res.users'].search([('login', '=', 'admin')])
website_visitor = Visitor.search([('partner_id', '=', user.partner_id.id)])
if not website_visitor:
with MockRequest(user.with_user(user).env, website=self.env['website'].get_current_website()):
website_visitor = Visitor.create({'partner_id': user.partner_id.id})
self.assertEqual(website_visitor.name, user.name, "The visitor should be linked to the admin user, not OdooBot or anything.")
self.product = self.env['product.product'].create({
'name': 'Storage Box',
'website_published': True,
'image_512': b'/product/static/img/product_product_9-image.jpg',
'display_name': 'Bin',
'description_sale': 'Pedal-based opening system',
})
before_tour_product_ids = website_visitor.product_ids.ids
website_visitor._add_viewed_product(self.product.id)
self.start_tour('/', 'website_sale.products_snippet_recently_viewed', login='admin')
self.assertEqual(before_tour_product_ids, website_visitor.product_ids.ids, "There shouldn't be any new product in recently viewed after this tour")
def test_website_category_url(self):
# Create a public category with a cover image
category = self.env['product.public.category'].create({
'name': "Test Category",
})
self.env.company.website_id = False
website = self.env.ref('website.default_website')
website.update({
'domain': "http://www.example.com",
'company_id': self.env.company.id,
})
# Simulate a request with correct context
with MockRequest(self.env, website=website):
original_get_base_url = self.env['product.public.category'].sudo().get_base_url()
data = self.env['website.snippet.filter'].sudo()._prepare_category_list_data(
parent_id=category.id,
)
# Assert that the returned cover_image uses the mocked base URL
self.assertTrue(data[0]['cover_image'].startswith(original_get_base_url))
self.assertNotIn(website.domain, data[0]['cover_image'])

View file

@ -0,0 +1,15 @@
from odoo.tests import tagged
from odoo.addons.website.tests.test_website_technical_page import TestWebsiteTechnicalPage
@tagged("post_install", "-at_install")
class TestWebsiteSaleTechnicalPage(TestWebsiteTechnicalPage):
def test_load_website_sale_technical_pages(self):
self._validate_routes([
"/shop",
"/shop/confirmation",
"/shop/extra_info",
"/shop/payment",
"/shop/checkout"
])

View file

@ -1,37 +1,32 @@
# coding: utf-8
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website.tools import MockRequest
from odoo.tests import TransactionCase, tagged
from odoo.addons.website_sale.tests.common import MockRequest, WebsiteSaleCommon
@tagged('post_install', '-at_install')
class WebsiteSaleVisitorTests(TransactionCase):
class WebsiteSaleVisitorTests(WebsiteSaleCommon):
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)
cookies = self.WebsiteSaleController.products_recently_viewed_update(self.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)
with MockRequest(self.env, website=self.website, cookies=cookies):
self.WebsiteSaleController.products_recently_viewed_update(self.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)])
@ -44,7 +39,7 @@ class WebsiteSaleVisitorTests(TransactionCase):
'list_price': 320.0,
})
with MockRequest(self.env, website=self.website, cookies=self.cookies):
with MockRequest(self.env, website=self.website, cookies=cookies):
self.WebsiteSaleController.products_recently_viewed_update(product.id)
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
@ -58,7 +53,6 @@ class WebsiteSaleVisitorTests(TransactionCase):
new_company = self.env['res.company'].create({
'name': 'Test Company',
})
public_user = self.env.ref('base.public_user')
product = self.env['product.product'].create({
'name': 'Test Product',
@ -66,17 +60,19 @@ class WebsiteSaleVisitorTests(TransactionCase):
'sale_ok': True,
})
self.website = self.website.with_user(public_user).with_context(website_id=self.website.id)
snippet_filter = self.env.ref('website_sale.dynamic_filter_newest_products')
website = self.website.with_user(self.public_user)
with MockRequest(website.env, website=website):
snippet_filter = self.env.ref('website_sale.dynamic_filter_newest_products')
res = snippet_filter._prepare_values(limit=16, search_domain=[])
res = snippet_filter._prepare_values(16, [])
res_products = [res_product['_record'] for res_product in res]
self.assertIn(product, res_products)
product.product_tmpl_id.company_id = new_company
product.product_tmpl_id.flush_recordset(['company_id'])
res = snippet_filter._prepare_values(16, [])
with MockRequest(website.env, website=website):
res = snippet_filter._prepare_values(limit=16, search_domain=[])
res_products = [res_product['_record'] for res_product in res]
self.assertNotIn(product, res_products)
@ -100,20 +96,20 @@ class WebsiteSaleVisitorTests(TransactionCase):
snippet_filter = self.env.ref('website_sale.dynamic_filter_latest_viewed_products')
# BEFORE VISITING THE PRODUCT
res = snippet_filter._prepare_values(16, [])
res = snippet_filter._prepare_values(limit=16, search_domain=[])
self.assertFalse(res)
# AFTER VISITING THE PRODUCT
with MockRequest(self.website.env, website=self.website):
self.cookies = self.WebsiteSaleController.products_recently_viewed_update(product.id)
with MockRequest(self.website.env, website=self.website, cookies=self.cookies):
res = snippet_filter._prepare_values(16, [])
cookies = self.WebsiteSaleController.products_recently_viewed_update(product.id)
with MockRequest(self.website.env, website=self.website, cookies=cookies):
res = snippet_filter._prepare_values(limit=16, search_domain=[])
res_products = [res_product['_record'] for res_product in res]
self.assertIn(product, res_products)
# AFTER CHANGING PRODUCT COMPANY
product.product_tmpl_id.company_id = new_company
product.product_tmpl_id.flush_recordset(['company_id'])
with MockRequest(self.website.env, website=self.website, cookies=self.cookies):
res = snippet_filter._prepare_values(16, [])
with MockRequest(self.website.env, website=self.website, cookies=cookies):
res = snippet_filter._prepare_values(limit=16, search_domain=[])
self.assertFalse(res)

View file

@ -1,27 +1,46 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from datetime import datetime, timedelta
from freezegun import freeze_time
from odoo.api import Environment
from odoo.tests import tagged
from odoo.tools import SQL
from odoo.addons.base.tests.common import BaseCommon
from odoo.addons.http_routing.tests.common import MockRequest
@odoo.tests.common.tagged('post_install', '-at_install')
class TestWebsiteSequence(odoo.tests.TransactionCase):
@tagged('post_install', '-at_install')
class TestWebsiteSequence(BaseCommon):
def setUp(self):
super(TestWebsiteSequence, self).setUp()
@classmethod
def setUpClass(cls):
super().setUpClass()
ProductTemplate = self.env['product.template']
cls.website = cls.env.ref('website.default_website')
cls.public_user = cls.env.ref('base.public_user')
ProductTemplate = cls.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'):
if 'orderpoint_ids' in cls.env['product.product']:
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.program' in self.env:
programs = self.env['loyalty.program'].search([])
if 'loyalty.program' in cls.env:
programs = cls.env['loyalty.program'].search([])
programs.active = False
programs.coupon_ids.unlink()
programs.unlink()
# The "Service on Timesheet" product cannot be archived nor deleted via ORM
if time_product := cls.env.ref('sale_timesheet.time_product', raise_if_not_found=False):
product_templates -= time_product.product_tmpl_id
cls.env.cr.execute(SQL(
'UPDATE product_template SET active = false WHERE id = %s',
time_product.product_tmpl_id.id,
))
product_templates.write({'active': False})
self.p1, self.p2, self.p3, self.p4 = ProductTemplate.create([{
cls.product_tmpls = cls.p1, cls.p2, cls.p3, cls.p4 = ProductTemplate.create([{
'name': 'First Product',
'website_sequence': 100,
}, {
@ -35,33 +54,48 @@ class TestWebsiteSequence(odoo.tests.TransactionCase):
'website_sequence': 250,
}])
self._check_correct_order(self.p1 + self.p2 + self.p3 + self.p4)
def get_product_sort_mapping(self, label):
context = dict(self.env.context, website_id=self.website.id, lang='en_US')
env = Environment(self.env.cr, self.public_user.id, context)
with MockRequest(env, website=self.website.with_env(env)) as req:
product_sort_mapping = req.env['website']._get_product_sort_mapping()
return next(k for k, v in product_sort_mapping if v == label)
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 get_sorted_products(self, order, products=None):
products = products or self.product_tmpls
return products.search(
[('id', 'in', products.ids)],
order=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 assertProductOrdering(self, products, order):
"""Assert `products` are sorted by `order`.
:param records products: The products or product templates to check.
:param str order: Expect ordering, in the same format as used by `search`.
"""
expected = self.get_sorted_products(order, products=products)
self.assertSequenceEqual(products, expected, f"Products should be ordered on '{order}'")
def test_01_website_sequence(self):
sequence_order = self.get_product_sort_mapping("Featured")
self.assertProductOrdering(self.p1 + self.p2 + self.p3 + self.p4, sequence_order)
# 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.assertProductOrdering(self.p1 + self.p3 + self.p2 + self.p4, sequence_order)
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.assertProductOrdering(self.p1 + self.p3 + self.p4 + self.p2, sequence_order)
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.assertProductOrdering(self.p2 + self.p1 + self.p3 + self.p4, sequence_order)
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)
self.assertProductOrdering(self.p2 + self.p3 + self.p4 + self.p1, sequence_order)
current_sequences = self._search_website_sequence_order().mapped('website_sequence')
current_products = self.get_sorted_products(sequence_order)
current_sequences = current_products.mapped('website_sequence')
self.assertEqual(current_sequences, [95, 180, 225, 230], "Wrong sequence order (2)")
self.p2.website_sequence = 1
@ -72,5 +106,44 @@ class TestWebsiteSequence(odoo.tests.TransactionCase):
new_product = self.env['product.template'].create({
'name': 'Last Newly Created Product',
})
current_products += new_product
self.assertEqual(self._search_website_sequence_order()[-1], new_product, "new product should be last")
self.assertEqual(
self.get_sorted_products(sequence_order, current_products)[-1],
new_product,
"New product should be last",
)
def test_02_newest_arrivals(self):
def toggle_publish(products, delta=timedelta(seconds=5)):
publish_date = datetime.now()
for product in products:
publish_date += delta
with freeze_time(publish_date):
product.website_publish_button()
product.flush_recordset() # force computations
newest_arrival_order = self.get_product_sort_mapping("Newest Arrivals")
toggle_publish(self.product_tmpls)
# Products were published sequentially,
# so first product is "oldest" arrival & last product is "newest" arrival
target = self.product_tmpls[::-1]
self.assertTrue(all(self.product_tmpls.mapped('is_published')))
self.assertProductOrdering(target, newest_arrival_order)
publish_dates = self.product_tmpls.mapped('publish_date')
toggle_publish(self.product_tmpls)
self.assertFalse(any(self.product_tmpls.mapped('is_published')))
self.assertSequenceEqual(
self.product_tmpls.mapped('publish_date'),
publish_dates,
"Unpublishing should not affect publishing date",
)
toggle_publish(self.p2, delta=timedelta(days=1))
self.assertEqual(
self.get_sorted_products(newest_arrival_order)[0],
self.p2,
"Most recently published product should appear first",
)