Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_customize
from . import test_express_checkout_flows
from . import test_sale_process
from . import test_sitemap
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_mail
from . import test_website_sale_pricelist
from . import test_website_sale_product_attribute_value_config
from . import test_website_sale_image
from . import test_website_sequence
from . import test_website_sale_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_reorder_from_portal
from . import test_website_sale_snippets
from . import test_website_sale_fiscal_position
from . import test_website_sale_invoice

View file

@ -0,0 +1,459 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.modules.module import get_module_resource
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
def setUp(self):
super().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,
})
tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 10})
product_template.taxes_id = tax
product_attribute = self.env['product.attribute'].create({
'name': 'Legs',
'visibility': 'visible',
'sequence': 10,
})
product_attribute_value_1 = self.env['product.attribute.value'].create({
'name': 'Steel - Test',
'attribute_id': product_attribute.id,
'sequence': 1,
})
product_attribute_value_2 = self.env['product.attribute.value'].create({
'name': 'Aluminium',
'attribute_id': product_attribute.id,
'sequence': 2,
})
# set attribute and attribute values on the template
self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, [product_attribute_value_1.id, product_attribute_value_2.id])]
}])
# set a different price on the variants to differentiate them
product_template_attribute_values = self.env['product.template.attribute.value'] \
.search([('product_tmpl_id', '=', product_template.id)])
for ptav in product_template_attribute_values:
if ptav.name == "Steel - Test":
ptav.price_extra = 0
else:
ptav.price_extra = 50.4
# 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})
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_02_admin_shop_custom_attribute_value_tour(self):
# Make sure pricelist rule exist
self.product_attribute_1 = self.env['product.attribute'].create({
'name': 'Legs',
'sequence': 10,
})
product_attribute_value_1 = self.env['product.attribute.value'].create({
'name': 'Steel',
'attribute_id': self.product_attribute_1.id,
'sequence': 1,
})
product_attribute_value_2 = self.env['product.attribute.value'].create({
'name': 'Aluminium',
'attribute_id': self.product_attribute_1.id,
'sequence': 2,
})
product_attribute_2 = self.env['product.attribute'].create({
'name': 'Color',
'sequence': 20,
})
product_attribute_value_3 = self.env['product.attribute.value'].create({
'name': 'White',
'attribute_id': product_attribute_2.id,
'sequence': 1,
})
product_attribute_value_4 = self.env['product.attribute.value'].create({
'name': 'Black',
'attribute_id': product_attribute_2.id,
'sequence': 2,
})
# Create product template
self.product_product_4_product_template = self.env['product.template'].create({
'name': 'Customizable Desk (TEST)',
'standard_price': 500.0,
'list_price': 750.0,
})
# Generate variants
self.env['product.template.attribute.line'].create([{
'product_tmpl_id': self.product_product_4_product_template.id,
'attribute_id': self.product_attribute_1.id,
'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
}, {
'product_tmpl_id': self.product_product_4_product_template.id,
'attribute_id': product_attribute_2.id,
'value_ids': [(4, product_attribute_value_3.id), (4, product_attribute_value_4.id)],
}])
product_template = self.product_product_4_product_template
# Add Custom Attribute
product_attribute_value_7 = self.env['product.attribute.value'].create({
'name': 'Custom TEST',
'attribute_id': self.product_attribute_1.id,
'sequence': 3,
'is_custom': True
})
self.product_product_4_product_template.attribute_line_ids[0].write({'value_ids': [(4, product_attribute_value_7.id)]})
img_path = get_module_resource('product', 'static', 'img', 'product_product_11-image.png')
img_content = base64.b64encode(open(img_path, "rb").read())
self.product_product_11_product_template = self.env['product.template'].create({
'name': 'Conference Chair (TEST)',
'website_sequence': 9999, # laule
'image_1920': img_content,
'list_price': 16.50,
})
self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.product_product_11_product_template.id,
'attribute_id': self.product_attribute_1.id,
'value_ids': [(4, product_attribute_value_1.id), (4, product_attribute_value_2.id)],
})
self.product_product_11_product_template.attribute_line_ids[0].product_template_value_ids[1].price_extra = 6.40
# Setup a second optional product
self.product_product_1_product_template = self.env['product.template'].create({
'name': 'Chair floor protection',
'list_price': 12.0,
})
# fix runbot, sometimes one pricelist is chosen, sometimes the other...
pricelists = self.env['website'].get_current_website().get_current_pricelist() | self.env.ref('product.list0')
for pricelist in pricelists:
if not pricelist.item_ids.filtered(lambda i: i.product_tmpl_id == product_template and i.price_discount == 20):
self.env['product.pricelist.item'].create({
'base': 'list_price',
'applied_on': '1_product',
'pricelist_id': pricelist.id,
'product_tmpl_id': product_template.id,
'price_discount': 20,
'min_quantity': 2,
'compute_price': 'formula',
})
pricelist.discount_policy = 'without_discount'
self.start_tour("/", 'shop_custom_attribute_value', login="admin")
def test_03_public_tour_shop_dynamic_variants(self):
""" The goal of this test is to make sure product variants with dynamic
attributes can be created by the public user (when being added to cart).
"""
# create the attribute
product_attribute = self.env['product.attribute'].create({
'name': "Dynamic Attribute",
'create_variant': 'dynamic',
})
# create the attribute values
product_attribute_values = self.env['product.attribute.value'].create([{
'name': "Dynamic Value 1",
'attribute_id': product_attribute.id,
'sequence': 1,
}, {
'name': "Dynamic Value 2",
'attribute_id': product_attribute.id,
'sequence': 2,
}])
# create the template
product_template = self.env['product.template'].create({
'name': 'Dynamic Product',
'website_published': True,
'list_price': 10,
})
# set attribute and attribute values on the template
self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, product_attribute_values.ids)]
}])
# set a different price on the variants to differentiate them
product_template_attribute_values = self.env['product.template.attribute.value'] \
.search([('product_tmpl_id', '=', product_template.id)])
for ptav in product_template_attribute_values:
if ptav.name == "Dynamic Value 1":
ptav.price_extra = 10
else:
# 0 to not bother with the pricelist of the public user
ptav.price_extra = 0
self.start_tour("/", 'tour_shop_dynamic_variants')
def test_04_portal_tour_deleted_archived_variants(self):
"""The goal of this test is to make sure deleted and archived variants
are shown as impossible combinations.
Using "portal" to have various users in the tests.
"""
# create the attribute
product_attribute = self.env['product.attribute'].create({
'name': "My Attribute",
'create_variant': 'always',
})
# create the attribute values
product_attribute_values = self.env['product.attribute.value'].create([{
'name': "My Value 1",
'attribute_id': product_attribute.id,
'sequence': 1,
}, {
'name': "My Value 2",
'attribute_id': product_attribute.id,
'sequence': 2,
}, {
'name': "My Value 3",
'attribute_id': product_attribute.id,
'sequence': 3,
}])
# create the template
product_template = self.env['product.template'].create({
'name': 'Test Product 2',
'is_published': True,
})
# set attribute and attribute values on the template
self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, product_attribute_values.ids)]
}])
# set a different price on the variants to differentiate them
product_template_attribute_values = self.env['product.template.attribute.value'] \
.search([('product_tmpl_id', '=', product_template.id)])
product_template_attribute_values[0].price_extra = 10
product_template_attribute_values[1].price_extra = 20
product_template_attribute_values[2].price_extra = 30
# archive first combination (first variant)
product_template.product_variant_ids[0].active = False
# delete second combination (which is now first variant since cache has been cleared)
product_template.product_variant_ids[0].unlink()
self.start_tour("/", 'tour_shop_deleted_archived_variants', login="portal")
def test_05_demo_tour_no_variant_attribute(self):
"""The goal of this test is to make sure attributes no_variant are
correctly added to cart.
Using "demo" to have various users in the tests.
"""
# create the attribute
product_attribute_no_variant = self.env['product.attribute'].create({
'name': "No Variant Attribute",
'create_variant': 'no_variant',
})
# create the attribute value
product_attribute_value_no_variant = self.env['product.attribute.value'].create({
'name': "No Variant Value",
'attribute_id': product_attribute_no_variant.id,
})
# create the template
product_template = self.env['product.template'].create({
'name': 'Test Product 3',
'website_published': True,
})
# set attribute and attribute value on the template
ptal = self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute_no_variant.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, product_attribute_value_no_variant.ids)]
}])
# set a price on the value
ptal.product_template_value_ids.price_extra = 10
self.start_tour("/", 'tour_shop_no_variant_attribute', login="demo")
def test_06_admin_list_view_b2c(self):
self.env.ref('product.group_product_variant').write({'users': [(4, self.env.ref('base.user_admin').id)]})
# activate b2c
config = self.env['res.config.settings'].create({})
config.show_line_subtotals_tax_selection = "tax_included"
config.execute()
self.start_tour(self.env['website'].get_client_action_url('/shop?search=Test Product'), 'shop_list_view_b2c', login="admin")
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")
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
attributes only disabled other options if only one is missing or all are selected.
Using "portal" to have various users in the tests.
"""
attribute_1, attribute_2, attribute_3 = self.env['product.attribute'].create([
{
'name': 'Size',
'create_variant': 'always',
},
{
'name': 'Color',
'create_variant': 'always',
},
{
'name': 'Brand',
'create_variant': 'always',
},
])
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,
})
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
self.start_tour("/", 'tour_shop_archived_variant_multi', login="portal")
def test_09_pills_variant(self):
"""The goal of this test is to make sure that you can click anywhere on a pill
and still trigger a variant change. The radio input be visually hidden.
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,
})
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.start_tour("/", 'test_09_pills_variant', login="portal")
def test_10_shop_editor_set_product_ribbon(self):
self.start_tour("/", 'shop_editor_set_product_ribbon', login="admin")

View file

@ -0,0 +1,212 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from uuid import uuid4
from werkzeug import urls
from odoo import Command
from odoo.http import root
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.website_sale.controllers.main import WebsiteSale as WebsiteSaleController
@tagged('at_install')
class TestWebsiteSaleExpressCheckoutFlows(HttpCaseWithUserDemo):
""" The goal of this method class is to test the address management on
express checkout.
"""
@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.express_checkout_billing_values = {
'name': 'Express Checkout Partner',
'email': 'express@check.out',
'phone': '0000000000',
'street': 'ooo',
'street2': 'ppp',
'city': 'ooo',
'zip': '1200',
'country': 'US',
'state': 'WA',
}
# 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():
if key in ('state', 'country'):
value = partner[f'{key}_id'].code
else:
value = partner[key]
self.assertEqual(value, expected, "Shipping value should match")
if partner.state_id:
self.assertEqual(
partner.state_id.country_id,
partner.country_id,
"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,
)
if not result.ok:
return {}
return result.json().get("result", {})
def test_express_checkout_public_user(self):
""" Test that when using express checkout as a public user, a new partner is created. """
session = self.authenticate(None, None)
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={
'billing_address': dict(self.express_checkout_billing_values)
}
)
new_partner = self.sale_order.partner_id
self.assertNotEqual(new_partner, self.website.user_id.partner_id)
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_billing_values,
)
def test_express_checkout_registered_user(self):
""" Test that when you use express checkout as a registered user and the address sent by the
express checkout form exactly matches the one registered 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)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
'billing_address': {
'name': self.user_demo.partner_id.name,
'email': self.user_demo.partner_id.email,
'phone': self.user_demo.partner_id.phone,
'street': self.user_demo.partner_id.street,
'street2': self.user_demo.partner_id.street2,
'city': self.user_demo.partner_id.city,
'zip': self.user_demo.partner_id.zip,
'country': self.user_demo.partner_id.country_id.code,
'state': self.user_demo.partner_id.state_id.code,
}
}
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
self.assertEqual(self.sale_order.partner_invoice_id.id, self.user_demo.partner_id.id)
def test_express_checkout_registered_user_existing_address(self):
""" Test that when you use the express checkout as a registered user and the address sent by
the express checkout form exactly matches to one of the addresses linked to this user in
odoo, we do not create a new partner and reuse the existing one.
"""
# Create a child partner for the demo partner
child_partner_address = dict(self.express_checkout_billing_values)
child_partner_country = self.env['res.country'].search([
('code', '=', child_partner_address.pop('country')),
], limit=1)
child_partner_state = self.env['res.country.state'].search([
('code', '=', child_partner_address.pop('state')),
('country_id', '=', child_partner_country.id),
], limit=1)
child_partner = self.env['res.partner'].create(dict(
**child_partner_address,
parent_id=self.user_demo.partner_id.id,
type='invoice',
country_id=child_partner_country.id,
state_id=child_partner_state.id,
))
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)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
'billing_address': dict(self.express_checkout_billing_values)
}
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
self.assertEqual(self.sale_order.partner_invoice_id.id, child_partner.id)
def test_express_checkout_registered_user_new_address(self):
""" Test that when you use the express checkout as a registered user and the address sent by
the express checkout form doesn't match to one of the addresses linked to this user 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)
self._make_json_rpc_request(
urls.url_join(
self.base_url(), WebsiteSaleController._express_checkout_route
), data={
'billing_address': dict(self.express_checkout_billing_values)
}
)
self.assertEqual(self.sale_order.partner_id.id, self.user_demo.partner_id.id)
new_partner = self.sale_order.partner_invoice_id
self.assertNotEqual(new_partner, self.website.user_id.partner_id)
self.assertPartnerShippingValues(
new_partner,
self.express_checkout_billing_values,
)

View file

@ -0,0 +1,508 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import odoo.tests
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
_logger = logging.getLogger(__name__)
@odoo.tests.tagged('post_install', '-at_install')
class TestUi(HttpCaseWithUserDemo):
def setUp(self):
super(TestUi, self).setUp()
product_product_7 = self.env['product.product'].create({
'name': 'Storage Box',
'standard_price': 70.0,
'list_price': 79.0,
'website_published': True,
})
self.product_attribute_1 = self.env['product.attribute'].create({
'name': 'Legs',
'sequence': 10,
})
product_attribute_value_1 = self.env['product.attribute.value'].create({
'name': 'Steel',
'attribute_id': self.product_attribute_1.id,
'sequence': 1,
})
product_attribute_value_2 = self.env['product.attribute.value'].create({
'name': 'Aluminium',
'attribute_id': self.product_attribute_1.id,
'sequence': 2,
})
self.product_product_11_product_template = self.env['product.template'].create({
'name': 'Conference Chair',
'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)],
})
self.product_product_1_product_template = self.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)]
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',
})
def test_01_admin_shop_tour(self):
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'shop', 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):
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')
tax_group = self.env['account.tax.group'].create({'name': 'Tax 15%'})
tax = self.env['account.tax'].create({
'name': 'Tax 15%',
'amount': 15,
'type_tax_use': 'sale',
'tax_group_id': tax_group.id
})
# storage box
self.product_product_7 = self.env['product.product'].create({
'name': 'Storage Box Test',
'standard_price': 70.0,
'list_price': 79.0,
'categ_id': self.env.ref('product.product_category_all').id,
'website_published': True,
'invoice_policy': 'delivery',
})
self.product_product_7.taxes_id = [tax.id]
self.env['res.config.settings'].create({
'auth_signup_uninvited': 'b2c',
'show_line_subtotals_tax_selection': 'tax_excluded',
'group_show_line_subtotals_tax_excluded': True,
'group_show_line_subtotals_tax_included': False,
}).execute()
self.start_tour("/", 'website_sale_tour_1')
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):
# Data for google_analytics_view_item
attribute = self.env['product.attribute'].create({
'name': 'Color',
'sequence': 10,
'display_type': 'color',
'value_ids': [
Command.create({
'name': 'Red',
}),
Command.create({
'name': 'Pink',
}),
]
})
self.env['product.template'].create({
'name': 'Colored T-Shirt',
'standard_price': 500,
'list_price': 750,
'detailed_type': 'consu',
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': attribute.value_ids,
})
]
})
self.env['website'].browse(1).write({'google_analytics_key': 'G-XXXXXXXXXXX'})
self.start_tour("/shop", 'google_analytics_view_item')
# Data for google_analytics_add_to_cart
self.env['product.template'].create({
'name': 'Basic Shirt',
'standard_price': 500,
'detailed_type': 'consu',
'website_published': True
})
self.start_tour("/shop", 'google_analytics_add_to_cart')
@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,
'sale_ok': True,
'taxes_id': [tax_10_incl.id]
})
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.assertEqual(
[so.amount_untaxed, so.amount_tax, so.amount_total],
[90.91, 9.09, 100.0]
)
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")

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged
@tagged('post_install', '-at_install')
class TestSitemap(HttpCase):
def setUp(self):
super(TestSitemap, self).setUp()
self.cats = self.env['product.public.category'].create([{
'name': 'Level 0',
}, {
'name': 'Level 1',
}, {
'name': 'Level 2',
}])
self.cats[2].parent_id = self.cats[1].id
self.cats[1].parent_id = self.cats[0].id
def test_01_shop_route_sitemap(self):
resp = self.url_open('/sitemap.xml')
level2_url = '/shop/category/level-0-level-1-level-2-%s' % self.cats[2].id
self.assertTrue(level2_url in resp.text, "Category entry in sitemap should be prefixed by its parent hierarchy.")

View file

@ -0,0 +1,266 @@
# -*- 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.exceptions import ValidationError
from odoo.tests import HttpCase, tagged
_logger = logging.getLogger(__name__)
ATTACHMENT_DATA = [
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKqf3geEAAA//8EGgIyYKYzzgAAAABJRU5ErkJggg==",
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKqvvQEEAAA//8EBQI0GMlQsAAAAABJRU5ErkJggg==",
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKKLakBBAAA//8ChwFQsvFlAwAAAABJRU5ErkJggg==",
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGJqkdoACAAA//8CfAFRzSyOUAAAAABJRU5ErkJggg==",
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGLSfxgICAAA//8CrAFkoLBhpQAAAABJRU5ErkJggg==",
]
ATTACHMENT_COUNT = 5
@tagged('post_install', '-at_install')
class TestProductPictureController(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': 'Storage Test Box',
'standard_price': 70.0,
'list_price': 79.0,
'website_published': True,
})
cls.attachments = cls.env['ir.attachment'].create([
{
'datas': ATTACHMENT_DATA[i],
'name': f'image0{i}.gif',
'public': True
}
for i in range(ATTACHMENT_COUNT)])
def _create_product_images(self):
with MockRequest(self.product.env, website=self.website):
self.WebsiteSaleController.add_product_images(
[{'id': attachment.id} for attachment in self.attachments],
self.product.id,
self.product.product_tmpl_id.id,
)
def _get_product_image_data(self):
return [
hasattr(image, 'video_url') and image.video_url or image.image_1920
for image in self.product._get_images()
]
def test_bulk_image_upload(self):
# Turns attachments to product_images
self._create_product_images()
# Check if the media now exists on the product :
for i, image in enumerate(self.product.product_template_image_ids):
# Check if all names are now in the product
self.assertIn(image.name, self.attachments.mapped('name'))
# Check if image datas are the same
self.assertEqual(image.image_1920, ATTACHMENT_DATA[i])
# Check if exactly ATTACHMENT_COUNT images were saved (no dupes/misses?)
self.assertEqual(ATTACHMENT_COUNT, len(self.product.product_template_image_ids))
def test_image_clear(self):
# First create some images
self._create_product_images()
self.assertEqual(ATTACHMENT_COUNT, len(self.product.product_template_image_ids))
# Remove all images
# (Exception raised if error)
with MockRequest(self.product.env, website=self.website):
self.WebsiteSaleController.clear_product_images(
self.product.id,
self.product.product_tmpl_id.id,
)
# According to the product, there are no variants images.
self.assertEqual(0, len(self.product.product_template_image_ids))
def test_extra_images_with_new_variant(self):
# Test that adding images for a variant that is not yet created works
product_attribute = self.env['product.attribute'].create({
"name": "Test attribute",
"create_variant": "dynamic",
})
product_attribute_values = self.env['product.attribute.value'].create([
{
"name" : "Test Dynamic 1",
"attribute_id": product_attribute.id,
"sequence": 1,
},
{
"name" : "Test Dynamic 2",
"attribute_id": product_attribute.id,
"sequence": 2,
}
])
product_template = self.env['product.template'].create({
"name": "test product",
"website_published": True,
})
product_template_attribute_line = self.env['product.template.attribute.line'].create({
"attribute_id": product_attribute.id,
"product_tmpl_id": product_template.id,
"value_ids": product_attribute_values,
})
self.assertEqual(0, len(product_template.product_variant_ids))
with MockRequest(product_template.env, website=self.website):
self.WebsiteSaleController.add_product_images(
[{'id': self.attachments[0].id}],
False,
product_template.id,
[product_template_attribute_line.product_template_value_ids[0].id],
)
self.assertEqual(1, len(product_template.product_variant_ids))
def test_resequence_image_first(self):
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'first',
)
# Trigger the reordering of product.image records based on their sequence.
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i3, i1, i2, i4, i5, i6])
self.assertEqual(self.product.image_1920, i3)
def test_resequence_image_left(self):
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'left',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i3, i2, i4, i5, i6])
def test_resequence_image_right(self):
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'right',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i2, i4, i3, i5, i6])
def test_resequence_image_last(self):
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'last',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i2, i4, i5, i6, i3])
def test_resequence_image_first_to_last(self):
""" Moving an image from first to last position is an edge case in the code. """
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[0]._name, images[0].id, 'last',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i2, i3, i4, i5, i6, i1])
self.assertEqual(self.product.image_1920, i2)
def test_resequence_video_left(self):
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
images[2].video_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'left',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i3, i2, i4, i5, i6])
def test_resequence_video_first(self):
""" A video can't be resequenced to first position. """
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
images[2].video_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
with self.assertRaises(ValidationError):
self.WebsiteSaleController.resequence_product_image(
images[2]._name, images[2].id, 'first',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i2, i3, i4, i5, i6])
def test_resequence_video_replace_first(self):
""" A video can't replace an image that was resequenced away from first position. """
self._create_product_images()
with MockRequest(self.product.env, website=self.website):
images = self.product._get_images()
images[1].video_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
i1, i2, i3, i4, i5, i6 = self._get_product_image_data()
with self.assertRaises(ValidationError):
self.WebsiteSaleController.resequence_product_image(
images[0]._name, images[0].id, 'right',
)
self.env['product.image'].invalidate_model()
self.assertListEqual(self._get_product_image_data(), [i1, i2, i3, i4, i5, i6])
@tagged('post_install', '-at_install')
class TestWebsiteSaleEditor(HttpCase):
@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
])]
})
def test_category_page_and_products_snippet(self):
category = self.env['product.public.category'].create({
'name': 'Test Category',
})
self.env['product.template'].create({
'name': 'Test Product',
'website_published': True,
'public_categ_ids': [
Command.link(category.id)
]
})
self.env['product.template'].create({
'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('/shop', 'category_page_and_products_snippet_use', login=None)
def test_website_sale_restricted_editor_ui(self):
self.env['product.template'].create({
'name': 'Test Product',
'website_sequence': 0,
'website_published': True,
})
self.start_tour(self.env['website'].get_client_action_url('/shop'), 'website_sale_restricted_editor_ui', login='restricted')

View file

@ -0,0 +1,83 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import Command
from odoo.tests import HttpCase, tagged
_logger = logging.getLogger(__name__)
@tagged('post_install', '-at_install')
class TestAddToCartSnippet(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create a dummy payment provider to ensure that the tour has at least one available to it.
arch = """
<form action="dummy" method="post">
<input type="hidden" name="view_id" t-att-value="viewid"/>
<input type="hidden" name="user_id" t-att-value="user_id.id"/>
</form>
"""
redirect_form = cls.env['ir.ui.view'].create({
'name': "Dummy Redirect Form",
'type': 'qweb',
'arch': arch,
})
cls.dummy_provider = cls.env['payment.provider'].create({
'name': "Dummy Provider",
'code': 'none',
'state': 'test',
'is_published': True,
'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id,
})
def test_configure_product(self):
# Reset the company country id, which ensure that no country dependant fields are blocking the address form.
self.env.company.country_id = self.env.ref('base.us')
attribute = self.env['product.attribute'].create({
'name': 'Color',
'value_ids': [
Command.create({
'name': 'Red'
}),
Command.create({
'name': 'Pink'
})
]
})
self.env['product.template'].create([{
'name': 'Product No Variant',
'website_published': True
}, {
'name': 'Product Yes Variant 1',
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': attribute.value_ids
})
]
}, {
'name': 'Product Yes Variant 2',
'website_published': True,
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': attribute.value_ids
})
]
}])
admin_partner = self.env.ref('base.user_admin').partner_id
admin_partner.write({
'street': "rue des Bourlottes, 9",
'street2': "",
'city': "Ramillies",
'zip': 1367,
'country_id': self.env.ref('base.be').id
})
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

@ -0,0 +1,228 @@
# coding: utf-8
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.fields import Command
@tagged('post_install', '-at_install')
class WebsiteSaleCart(TransactionCaseWithUserPortal):
@classmethod
def setUpClass(cls):
super(WebsiteSaleCart, cls).setUpClass()
cls.website = cls.env['website'].browse(1)
cls.WebsiteSaleController = WebsiteSale()
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({
'name': 'Test Product',
'sale_ok': True,
'website_published': True,
})
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_unpublished_product(self):
# Try to add an unpublished product
product = self.env['product.product'].create({
'name': 'Test Product',
'sale_ok': 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)
# public but remove sale_ok
product.sale_ok = False
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)
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
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_zero_price_product_rule(self):
"""
With the `prevent_zero_price_sale` that we have on website, we can't add free products
to our cart.
There is an exception for certain product types specified by the
`_get_product_types_allow_zero_price` method, so this test ensures that it works
by mocking that function to return the "service" product type.
"""
website_prevent_zero_price = self.env['website'].create({
'name': 'Prevent zero price sale',
'prevent_zero_price_sale': True,
})
product_consu = self.env['product.product'].create({
'name': 'Cannot be zero price',
'detailed_type': 'consu',
'list_price': 0,
'website_published': True,
})
product_service = self.env['product.product'].create({
'name': 'Can be zero price',
'detailed_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)
# service types should not raise a UserError
with MockRequest(self.env, website=website_prevent_zero_price):
self.WebsiteSaleController.cart_update_json(product_id=product_service.id, add_qty=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()
sale_order.access_token = 'test_token'
old_amount = sale_order.amount_total
self.WebsiteSaleController.cart_update_json(product_id=product.id, add_qty=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)
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):
# 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.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.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.assertEqual(sale_order.order_line, SaleOrderLine)
def test_unpublished_accessory_product_visibility(self):
# Check if unpublished product is shown to public user
accessory_product = self.env['product.product'].create({
'name': 'Access Product',
'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)]
})
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)
def test_remove_archived_product_line(self):
"""If an order has a line containing an archived product,
it is removed when opening the order in the cart."""
# Arrange
user = self.public_user
website = self.website.with_user(user)
product = self.env['product.product'].create({
'name': 'Product',
'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()
# pre-condition: the order contains an active product
self.assertRecordValues(order.order_line, [{
"product_id": product.id,
}])
self.assertTrue(product.active)
# Act: archive the product and open the cart
product.active = False
self.WebsiteSaleController.cart()
# Assert: the line has been removed
self.assertFalse(order.order_line)
def test_keep_note_line(self):
"""If an order has a line containing a note,
it is not removed when opening the order in the cart."""
# 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)
order.order_line = [
Command.create({
"name": "Note",
"display_type": "line_note",
})
]
# pre-condition: the order contains only a note line
self.assertRecordValues(order.order_line, [{
"display_type": "line_note",
}])
# Act: open the cart
self.WebsiteSaleController.cart()
# Assert: the line is still there
self.assertRecordValues(order.order_line, [{
"display_type": "line_note",
}])

View file

@ -0,0 +1,255 @@
# -*- 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 odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
from odoo.addons.mail.models.mail_template import MailTemplate
class TestWebsiteSaleCartAbandonedCommon(HttpCaseWithUserPortal):
@classmethod
def setUpClass(cls):
super().setUpClass()
now = datetime.utcnow()
cls.customer = cls.env['res.partner'].create({
'name': 'a',
'email': 'a@example.com',
})
cls.public_partner = cls.env['res.partner'].create({
'name': 'public',
'email': 'public@example.com',
})
cls.public_user = cls.env['res.users'].create({
'name': 'Foo', 'login': 'foo',
'partner_id': cls.public_partner.id,
})
cls.website0 = cls.env['website'].create({
'name': 'web0',
'cart_abandoned_delay': 1.0, # 1 hour
})
cls.website1 = cls.env['website'].create({
'name': 'web1',
'cart_abandoned_delay': 0.5, # 30 minutes
})
cls.website2 = cls.env['website'].create({
'name': 'web2',
'cart_abandoned_delay': 24.0, # 1 day
'user_id': cls.public_user.id, # specific public user
})
product = cls.env['product.product'].create({
'name': 'The Product'
})
add_order_line = [[0, 0, {
'name': 'The Product',
'product_id': product.id,
'product_uom_qty': 1,
}]]
cls.so0before = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website0.id,
'state': 'draft',
'date_order': (now - relativedelta(hours=1)) - relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so0after = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website0.id,
'state': 'draft',
'date_order': (now - relativedelta(hours=1)) + relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so1before = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website1.id,
'state': 'draft',
'date_order': (now - relativedelta(minutes=30)) - relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so1after = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website1.id,
'state': 'draft',
'date_order': (now - relativedelta(minutes=30)) + relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so2before = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website2.id,
'state': 'draft',
'date_order': (now - relativedelta(hours=24)) - relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so2after = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'website_id': cls.website2.id,
'state': 'draft',
'date_order': (now - relativedelta(hours=24)) + relativedelta(minutes=1),
'order_line': add_order_line,
})
cls.so2before_but_public = cls.env['sale.order'].create({
'partner_id': cls.public_partner.id,
'website_id': cls.website2.id,
'state': 'draft',
'date_order': (now - relativedelta(hours=24)) - relativedelta(minutes=1),
'order_line': add_order_line,
})
# Must behave like so1before because public partner is not the one of website1
cls.so1before_but_other_public = cls.env['sale.order'].create({
'partner_id': cls.public_partner.id,
'website_id': cls.website1.id,
'state': 'draft',
'date_order': (now - relativedelta(minutes=30)) - relativedelta(minutes=1),
'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']
abandoned = SaleOrder.search([('is_abandoned_cart', '=', True)]).ids
self.assertTrue(self.so0before.id in abandoned)
self.assertTrue(self.so1before.id in abandoned)
self.assertTrue(self.so1before_but_other_public.id in abandoned)
self.assertTrue(self.so2before.id in abandoned)
self.assertFalse(self.so0after.id in abandoned)
self.assertFalse(self.so1after.id in abandoned)
self.assertFalse(self.so2after.id in abandoned)
self.assertFalse(self.so2before_but_public.id in abandoned)
non_abandoned = SaleOrder.search([('is_abandoned_cart', '=', False)]).ids
self.assertFalse(self.so0before.id in non_abandoned)
self.assertFalse(self.so1before.id in non_abandoned)
self.assertFalse(self.so1before_but_other_public.id in non_abandoned)
self.assertFalse(self.so2before.id in non_abandoned)
self.assertTrue(self.so0after.id in non_abandoned)
self.assertTrue(self.so1after.id in non_abandoned)
self.assertTrue(self.so2after.id in non_abandoned)
self.assertFalse(self.so2before_but_public.id in abandoned)
def test_website_sale_abandoned_cart_email(self):
"""Make sure the send_abandoned_cart_email method sends the correct emails."""
website = self.env['website'].get_current_website()
website.send_abandoned_cart_email = True
product = self.env['product.product'].create({
'name': 'The Product'
})
order_line = [[0, 0, {
'name': 'The Product',
'product_id': product.id,
'product_uom_qty': 1,
}]]
abandoned_sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(minutes=1),
'order_line': order_line
})
self.assertTrue(abandoned_sale_order.is_abandoned_cart)
self.assertTrue(self.send_mail_patched(abandoned_sale_order.id))
# Test that no mail is sent if the partner has no email address.
self.customer.email = False
self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(
minutes=1),
'order_line': order_line
})
self.assertFalse(self.send_mail_patched(abandoned_sale_order.id))
# Test that no mail is sent if the recovery email of the sale order has already been sent.
self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(
minutes=1),
'order_line': order_line,
'cart_recovery_email_sent': True
})
self.assertFalse(self.send_mail_patched(abandoned_sale_order.id))
# Test that no email is sent if the sale order contains product that are free.
free_product_template = self.env['product.template'].create({
'list_price': 0.0,
'name': 'free_product'
})
free_product_product = free_product_template.product_variant_id
order_line = [[0, 0, {
'name': 'The Product',
'product_id': free_product_product.id,
'product_uom_qty': 1,
}]]
self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(
minutes=1),
'order_line': order_line
})
self.assertFalse(self.send_mail_patched(abandoned_sale_order.id))
# Test that no email is sent if the sale order has no error in its transaction.
abandoned_sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(
minutes=1),
'order_line': order_line,
})
transaction = self.env['payment.transaction'].create({
'provider_id': 15,
'partner_id': self.customer.id,
'reference': abandoned_sale_order.name,
'amount': abandoned_sale_order.amount_total,
'state': 'error',
'currency_id': self.env.ref('base.EUR').id,
})
abandoned_sale_order.transaction_ids += transaction
self.assertFalse(self.send_mail_patched(abandoned_sale_order.id))
# Test that if the partner of the abandoned cart made an order ulterior to the abandoned cart create date,
# no email is sent.
self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': (datetime.utcnow() - relativedelta(hours=website.cart_abandoned_delay)) - relativedelta(
minutes=1),
'order_line': order_line,
})
self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': website.id,
'state': 'draft',
'date_order': datetime.utcnow(),
'order_line': order_line,
})
self.assertFalse(self.send_mail_patched(abandoned_sale_order.id))

View file

@ -0,0 +1,54 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.models import Command
from odoo.tests.common import tagged
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.website.tools import MockRequest
@tagged('post_install', '-at_install')
class WebsiteSaleCartPayment(PaymentCommon):
@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({
'amount': cls.amount,
'currency_id': cls.currency.id,
'provider_id': cls.provider.id,
'reference': cls.reference,
'operation': 'online_redirect',
'partner_id': cls.partner.id,
})
cls.order.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):
self.assertEqual(
self.website.sale_get_order(),
self.order,
msg=f"The transaction state '{unpaid_order_tx_state}' should not prevent "
f"retrieving the linked order.",
)
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
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):
self.assertFalse(
self.website.sale_get_order(),
msg=f"The transaction state '{paid_order_tx_state}' should prevent retrieving "
f"the linked order.",
)

View file

@ -0,0 +1,20 @@
# -*- 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

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tests.common import HttpCase, TransactionCase
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartRecovery(HttpCaseWithUserPortal):
def test_01_shop_cart_recovery_tour(self):
"""The goal of this test is to make sure cart recovery works."""
self.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'list_price': 2950.0,
'website_published': True,
})
self.start_tour("/", 'shop_cart_recovery', login="portal")
@tagged('post_install', '-at_install')
class TestWebsiteSaleCartRecoveryServer(TransactionCase):
def setUp(self):
res = super(TestWebsiteSaleCartRecoveryServer, self).setUp()
self.customer = self.env['res.partner'].create({
'name': 'a',
'email': 'a@example.com',
})
self.recovery_template_default = self.env.ref('website_sale.mail_template_sale_cart_recovery')
self.recovery_template_custom1 = self.recovery_template_default.copy()
self.recovery_template_custom2 = self.recovery_template_default.copy()
self.website0 = self.env['website'].create({
'name': 'web0',
'cart_recovery_mail_template_id': self.recovery_template_default.id,
})
self.website1 = self.env['website'].create({
'name': 'web1',
'cart_recovery_mail_template_id': self.recovery_template_custom1.id,
})
self.website2 = self.env['website'].create({
'name': 'web2',
'cart_recovery_mail_template_id': self.recovery_template_custom2.id,
})
self.so0 = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': self.website0.id,
'is_abandoned_cart': True,
'cart_recovery_email_sent': False,
})
self.so1 = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': self.website1.id,
'is_abandoned_cart': True,
'cart_recovery_email_sent': False,
})
self.so2 = self.env['sale.order'].create({
'partner_id': self.customer.id,
'website_id': self.website2.id,
'is_abandoned_cart': True,
'cart_recovery_email_sent': False,
})
return res
def test_cart_recovery_mail_template(self):
"""Make sure that we get the correct cart recovery templates to send."""
self.assertEqual(
self.so1._get_cart_recovery_template(),
self.recovery_template_custom1,
"We do not return the correct mail template"
)
self.assertEqual(
self.so2._get_cart_recovery_template(),
self.recovery_template_custom2,
"We do not return the correct mail template"
)
# Orders that belong to different websites; we should get the default template
self.assertEqual(
(self.so1 + self.so2)._get_cart_recovery_template(),
self.recovery_template_default,
"We do not return the correct mail template"
)
def test_cart_recovery_mail_template_send(self):
"""The goal of this test is to make sure cart recovery works."""
orders = self.so0 + self.so1 + self.so2
self.assertFalse(
any(orders.mapped('cart_recovery_email_sent')),
"The recovery mail should not have been sent yet."
)
self.assertFalse(
any(orders.mapped('access_token')),
"There should not be an access token yet."
)
orders._cart_recovery_email_send()
self.assertTrue(
all(orders.mapped('cart_recovery_email_sent')),
"The recovery mail should have been sent."
)
self.assertTrue(
all(orders.mapped('access_token')),
"All tokens should have been generated."
)
sent_mail = {}
for order in orders:
mail = self.env["mail.mail"].search([
('record_name', '=', order['name'])
])
sent_mail.update({order: mail})
self.assertTrue(
all(len(sent_mail[order]) == 1 for order in orders),
"Each cart recovery mail has been sent exactly once."
)
self.assertTrue(
all(order.access_token in sent_mail[order].body for order in orders),
"Each mail should contain the access token of the corresponding SO."
)

View file

@ -0,0 +1,120 @@
# -*- 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,429 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
from PIL import Image
from odoo.tests.common import HOST
from odoo.tools import config
import odoo.tests
@odoo.tests.common.tagged('post_install', '-at_install')
class TestWebsiteSaleImage(odoo.tests.HttpCase):
# registry_test_mode = False # uncomment to save the product to test in browser
def test_01_admin_shop_zoom_tour(self):
color_red = '#CD5C5C'
name_red = 'Indian Red'
color_green = '#228B22'
name_green = 'Forest Green'
color_blue = '#4169E1'
name_blue = 'Royal Blue'
# create the color attribute
product_attribute = self.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
})
# create the color attribute values
attr_values = self.env['product.attribute.value'].create([{
'name': name_red,
'attribute_id': product_attribute.id,
'html_color': color_red,
'sequence': 1,
}, {
'name': name_green,
'attribute_id': product_attribute.id,
'html_color': color_green,
'sequence': 2,
}, {
'name': name_blue,
'attribute_id': product_attribute.id,
'html_color': color_blue,
'sequence': 3,
}])
# first image (blue) for the template
f = io.BytesIO()
Image.new('RGB', (1920, 1080), color_blue).save(f, 'JPEG')
f.seek(0)
blue_image = base64.b64encode(f.read())
# second image (red) for the variant 1, small image (no zoom)
f = io.BytesIO()
Image.new('RGB', (800, 500), color_red).save(f, 'JPEG')
f.seek(0)
red_image = base64.b64encode(f.read())
# second image (green) for the variant 2, big image (zoom)
f = io.BytesIO()
Image.new('RGB', (1920, 1080), color_green).save(f, 'JPEG')
f.seek(0)
green_image = base64.b64encode(f.read())
# Template Extra Image 1
f = io.BytesIO()
Image.new('RGB', (124, 147)).save(f, 'GIF')
f.seek(0)
image_gif = base64.b64encode(f.read())
# Template Extra Image 2
image_svg = base64.b64encode(b'<svg></svg>')
# Red Variant Extra Image 1
f = io.BytesIO()
Image.new('RGB', (767, 247)).save(f, 'BMP')
f.seek(0)
image_bmp = base64.b64encode(f.read())
# Green Variant Extra Image 1
f = io.BytesIO()
Image.new('RGB', (2147, 3251)).save(f, 'PNG')
f.seek(0)
image_png = base64.b64encode(f.read())
# create the template, without creating the variants
template = self.env['product.template'].with_context(create_product_product=True).create({
'name': 'A Colorful Image',
'product_template_image_ids': [(0, 0, {'name': 'image 1', 'image_1920': image_gif}), (0, 0, {'name': 'image 4', 'image_1920': image_svg})],
})
# set the color attribute and values on the template
line = self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': template.id,
'value_ids': [(6, 0, attr_values.ids)]
}])
value_red = line.product_template_value_ids[0]
value_green = line.product_template_value_ids[1]
# set a different price on the variants to differentiate them
product_template_attribute_values = self.env['product.template.attribute.value'].search([('product_tmpl_id', '=', template.id)])
for val in product_template_attribute_values:
if val.name == name_red:
val.price_extra = 10
else:
val.price_extra = 20
# Get RED variant, and set image to blue (will be set on the template
# because the template image is empty and there is only one variant)
product_red = template._get_variant_for_combination(value_red)
product_red.write({
'image_1920': blue_image,
'product_variant_image_ids': [(0, 0, {'name': 'image 2', 'image_1920': image_bmp})],
})
self.assertEqual(template.image_1920, blue_image)
# Get the green variant
product_green = template._get_variant_for_combination(value_green)
product_green.write({
'image_1920': green_image,
'product_variant_image_ids': [(0, 0, {'name': 'image 3', 'image_1920': image_png})],
})
# now set the red image on the first variant, that works because
# template image is not empty anymore and we have a second variant
product_red.image_1920 = red_image
# Verify image_1920 size > 1024 can be zoomed
self.assertTrue(template.can_image_1024_be_zoomed)
self.assertFalse(template.product_template_image_ids[0].can_image_1024_be_zoomed)
self.assertFalse(template.product_template_image_ids[1].can_image_1024_be_zoomed)
self.assertFalse(product_red.can_image_1024_be_zoomed)
self.assertFalse(product_red.product_variant_image_ids[0].can_image_1024_be_zoomed)
self.assertTrue(product_green.can_image_1024_be_zoomed)
self.assertTrue(product_green.product_variant_image_ids[0].can_image_1024_be_zoomed)
# jpeg encoding is changing the color a bit
jpeg_blue = (65, 105, 227)
jpeg_red = (205, 93, 92)
jpeg_green = (34, 139, 34)
# Verify original size: keep original
image = Image.open(io.BytesIO(base64.b64decode(template.image_1920)))
self.assertEqual(image.size, (1920, 1080))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_1920)))
self.assertEqual(image.size, (800, 500))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_1920)))
self.assertEqual(image.size, (1920, 1080))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
# Verify 1024 size: keep aspect ratio
image = Image.open(io.BytesIO(base64.b64decode(template.image_1024)))
self.assertEqual(image.size, (1024, 576))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_1024)))
self.assertEqual(image.size, (800, 500))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_1024)))
self.assertEqual(image.size, (1024, 576))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
# Verify 512 size: keep aspect ratio
image = Image.open(io.BytesIO(base64.b64decode(template.image_512)))
self.assertEqual(image.size, (512, 288))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_512)))
self.assertEqual(image.size, (512, 320))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_512)))
self.assertEqual(image.size, (512, 288))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
# Verify 256 size: keep aspect ratio
image = Image.open(io.BytesIO(base64.b64decode(template.image_256)))
self.assertEqual(image.size, (256, 144))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_256)))
self.assertEqual(image.size, (256, 160))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_256)))
self.assertEqual(image.size, (256, 144))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
# Verify 128 size: keep aspect ratio
image = Image.open(io.BytesIO(base64.b64decode(template.image_128)))
self.assertEqual(image.size, (128, 72))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_blue, "blue")
image = Image.open(io.BytesIO(base64.b64decode(product_red.image_128)))
self.assertEqual(image.size, (128, 80))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_red, "red")
image = Image.open(io.BytesIO(base64.b64decode(product_green.image_128)))
self.assertEqual(image.size, (128, 72))
self.assertEqual(image.getpixel((image.size[0] / 2, image.size[1] / 2)), jpeg_green, "green")
# self.env.cr.commit() # uncomment to save the product to test in browser
# 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.
# 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.start_tour("/", 'shop_zoom', login="admin")
# CASE: unlink move image to fallback if fallback image empty
template.image_1920 = False
product_red.unlink()
self.assertEqual(template.image_1920, red_image)
# CASE: unlink does nothing special if fallback image already set
self.env['product.product'].create({
'product_tmpl_id': template.id,
'image_1920': green_image,
}).unlink()
self.assertEqual(template.image_1920, red_image)
# CASE: display variant image first if set
self.assertEqual(product_green._get_images()[0].image_1920, green_image)
# CASE: display variant fallback after variant o2m, correct fallback
# write on the variant field, otherwise it will write on the fallback
product_green.image_variant_1920 = False
images = product_green._get_images()
# images on fields are resized to max 1920
image_png = Image.open(io.BytesIO(base64.b64decode(images[1].image_1920)))
self.assertEqual(images[0].image_1920, red_image)
self.assertEqual(image_png.size, (1268, 1920))
self.assertEqual(images[2].image_1920, image_gif)
self.assertEqual(images[3].image_1920, image_svg)
# CASE: When uploading a product variant image
# we don't want the default_product_tmpl_id from the context to be applied if we have a product_variant_id set
# we want the default_product_tmpl_id from the context to be applied if we don't have a product_variant_id set
additionnal_context = {'default_product_tmpl_id': template.id}
product = self.env['product.product'].create({
'product_tmpl_id': template.id,
})
product_image = self.env['product.image'].with_context(**additionnal_context).create([{
'name': 'Template image',
'image_1920': red_image,
}, {
'name': 'Variant image',
'image_1920': blue_image,
'product_variant_id': product.id,
}])
template_image = product_image.filtered(lambda i: i.name == 'Template image')
variant_image = product_image.filtered(lambda i: i.name == 'Variant image')
self.assertEqual(template_image.product_tmpl_id.id, template.id)
self.assertFalse(template_image.product_variant_id.id)
self.assertFalse(variant_image.product_tmpl_id.id)
self.assertEqual(variant_image.product_variant_id.id, product.id)
def test_02_image_holder(self):
f = io.BytesIO()
Image.new('RGB', (800, 500), '#FF0000').save(f, 'JPEG')
f.seek(0)
image = base64.b64encode(f.read())
# create the color attribute
product_attribute = self.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
})
# create the color attribute values
attr_values = self.env['product.attribute.value'].create([{
'name': 'Red',
'attribute_id': product_attribute.id,
'sequence': 1,
}, {
'name': 'Green',
'attribute_id': product_attribute.id,
'sequence': 2,
}, {
'name': 'Blue',
'attribute_id': product_attribute.id,
'sequence': 3,
}])
# create the template, without creating the variants
template = self.env['product.template'].with_context(create_product_product=True).create({
'name': 'Test subject',
})
# when there are no variants, the image must be obtained from the template
self.assertEqual(template, template._get_image_holder())
# set the color attribute and values on the template
line = self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': template.id,
'value_ids': [(6, 0, attr_values.ids)]
}])
value_red = line.product_template_value_ids[0]
product_red = template._get_variant_for_combination(value_red)
product_red.image_variant_1920 = image
value_green = line.product_template_value_ids[1]
product_green = template._get_variant_for_combination(value_green)
product_green.image_variant_1920 = image
# when there are no template image but there are variants, the image must be obtained from the first variant
self.assertEqual(product_red, template._get_image_holder())
product_red.toggle_active()
# but when some variants are not available, the image must be obtained from the first available variant
self.assertEqual(product_green, template._get_image_holder())
template.image_1920 = image
# when there is a template image, the image must be obtained from the template
self.assertEqual(template, template._get_image_holder())
@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',
})
# First image (blue) for the template.
color_blue = '#4169E1'
name_blue = 'Royal Blue'
# Red for the variant.
color_red = '#CD5C5C'
name_red = 'Indian Red'
# Create the color attribute.
self.product_attribute = self.env['product.attribute'].create({
'name': 'Beautiful Color',
'display_type': 'color',
})
# create the color attribute values
self.attr_values = self.env['product.attribute.value'].create([{
'name': name_blue,
'attribute_id': self.product_attribute.id,
'html_color': color_blue,
'sequence': 1,
}, {
'name': name_red,
'attribute_id': self.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({
'name': 'Test Remove Image',
'image_1920': blue_image,
})
@odoo.tests.common.tagged('post_install', '-at_install')
class TestRemoveWebsiteSaleImageNoVariant(TestEnvironmentWebsiteSaleImage):
def setUp(self):
super(TestRemoveWebsiteSaleImageNoVariant, self).setUp()
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()
# Set the color attribute and values on the template.
self.env['product.template.attribute.line'].create([{
'attribute_id': self.product_attribute.id,
'product_tmpl_id': self.template.id,
'value_ids': [(6, 0, self.attr_values.ids)]
}])
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

@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
from odoo.addons.sale.tests.common import SaleCommon
@tagged('-at_install', 'post_install')
class TestWebsiteSaleInvoice(AccountPaymentCommon, SaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].create({
'name': 'Test Website'
})
def test_automatic_invoice_website_id(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Create SO on Test Website
self.sale_order.website_id = self.website.id
# Create the payment
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()
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

@ -0,0 +1,48 @@
# -*- 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.tests import tagged
from odoo.tests.common import HttpCase
@tagged('post_install', '-at_install')
class TestWebsiteSaleMail(HttpCase):
def test_01_shop_mail_tour(self):
"""The goal of this test is to make sure sending SO by email works."""
self.env['product.product'].create({
'name': 'Acoustic Bloc Screens',
'list_price': 2950.0,
'website_published': True,
})
self.env['res.partner'].create({
'name': 'Azure Interior',
'email': 'azure.Interior24@example.com',
})
# we override unlink because we don't want the email to be auto deleted
MailMail = odoo.addons.mail.models.mail_mail.MailMail
# as we check some link content, avoid mobile doing its link management
self.env['ir.config_parameter'].sudo().set_param('mail_mobile.disable_redirect_firebase_dynamic_link', True)
main_website = self.env.ref('website.default_website')
other_websites = self.env['website'].search([]) - main_website
# We change the domain of the website to test that the email that
# will be sent uses the correct domain for its links.
main_website.domain = "my-test-domain.com"
for w in other_websites:
w.domain = f'domain-not-used-{w.id}.fr'
with patch.object(MailMail, 'unlink', lambda self: None):
start_time = fields.Datetime.now()
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)
self.assertTrue(new_mail)
self.assertIn('Your', new_mail.body_html)
self.assertIn('order', new_mail.body_html)

View file

@ -0,0 +1,726 @@
# -*- 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 odoo.fields import Command
from odoo.tests import tagged, TransactionCase
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.addons.website.tools import MockRequest
_logger = logging.getLogger(__name__)
r''' /!\/!\
Calling `get_pricelist_available` after setting `property_product_pricelist` on
a partner will not work as expected. That field will change the output of
`get_pricelist_available` but modifying it will not invalidate the cache.
Thus, tests should not do:
self.env.user.partner_id.property_product_pricelist = my_pricelist
pls = self.get_pricelist_available()
self.assertEqual(...)
self.env.user.partner_id.property_product_pricelist = another_pricelist
pls = self.get_pricelist_available()
self.assertEqual(...)
as `_get_pl_partner_order` cache won't be invalidate between the calls, output
won't be the one expected and tests will actually not test anything.
Try to keep one call to `get_pricelist_available` by test method.
'''
@tagged('post_install', '-at_install')
class TestWebsitePriceList(TransactionCase):
def setUp(self):
super(TestWebsitePriceList, self).setUp()
self.env.user.partner_id.country_id = False # Remove country to avoid property pricelist computed.
self.website = self.env.ref('website.default_website')
self.website.user_id = self.env.user
(self.env['product.pricelist'].search([]) - self.env.ref('product.list0')).write({'website_id': False, 'active': False})
self.benelux = self.env['res.country.group'].create({
'name': 'BeNeLux',
'country_ids': [(6, 0, (self.env.ref('base.be') + self.env.ref('base.lu') + self.env.ref('base.nl')).ids)]
})
self.list_benelux = self.env['product.pricelist'].create({
'name': 'Benelux',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(4, self.benelux.id)],
'sequence': 2,
})
item_benelux = self.env['product.pricelist.item'].create({
'pricelist_id': self.list_benelux.id,
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'currency_id': self.env.ref('base.EUR').id,
})
self.list_christmas = self.env['product.pricelist'].create({
'name': 'Christmas',
'selectable': False,
'website_id': self.website.id,
'country_group_ids': [(4, self.env.ref('base.europe').id)],
'sequence': 20,
})
item_christmas = self.env['product.pricelist.item'].create({
'pricelist_id': self.list_christmas.id,
'compute_price': 'formula',
'base': 'list_price',
'price_discount': 20,
})
list_europe = self.env['product.pricelist'].create({
'name': 'EUR',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(4, self.env.ref('base.europe').id)],
'sequence': 3,
'currency_id': self.env.ref('base.EUR').id,
})
item_europe = self.env['product.pricelist.item'].create({
'pricelist_id': list_europe.id,
'compute_price': 'formula',
'base': 'list_price',
})
self.env.ref('product.list0').website_id = self.website.id
self.website.pricelist_id = self.ref('product.list0')
ca_group = self.env['res.country.group'].create({
'name': 'Canada',
'country_ids': [(6, 0, [self.ref('base.ca')])]
})
self.env['product.pricelist'].create({
'name': 'Canada',
'selectable': True,
'website_id': self.website.id,
'country_group_ids': [(6, 0, [ca_group.id])],
'sequence': 10
})
self.args = {
'show': False,
'current_pl': False,
}
patcher = patch('odoo.addons.website_sale.models.website.Website.get_pricelist_available', wraps=self._get_pricelist_available)
self.startPatcher(patcher)
# Mock nedded because request.session doesn't exist during test
def _get_pricelist_available(self, show_visible=False):
return self.get_pl(self.args.get('show'), self.args.get('current_pl'), self.args.get('country'))
def get_pl(self, show_visible, current_pl_id, country_code):
self.website.invalidate_recordset(['pricelist_ids'])
pl_ids = self.website._get_pl_partner_order(
country_code,
show_visible,
current_pl_id=current_pl_id,
website_pricelist_ids=tuple(self.website.pricelist_ids.ids),
)
return self.env['product.pricelist'].browse(pl_ids)
def test_get_pricelist_available_show(self):
show = True
current_pl = False
country_list = {
False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
'BE': ['EUR', 'Benelux'],
'IT': ['EUR'],
'CA': ['Canada'],
'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
}
for country, result in country_list.items():
pls = self.get_pl(show, current_pl, country)
self.assertEqual(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)'
% (country, len(pls), pls.mapped('name'), len(result), result))
def test_get_pricelist_available_not_show(self):
show = False
current_pl = False
country_list = {
False: ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
'BE': ['EUR', 'Benelux', 'Christmas'],
'IT': ['EUR', 'Christmas'],
'US': ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
'CA': ['Canada']
}
for country, result in country_list.items():
pls = self.get_pl(show, current_pl, country)
self.assertEqual(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)'
% (country, len(pls), pls.mapped('name'), len(result), result))
def test_get_pricelist_available_promocode(self):
christmas_pl = self.list_christmas.id
country_list = {
False: True,
'BE': True,
'IT': True,
'US': True,
'CA': False
}
for country, result in country_list.items():
self.args['country'] = country
# mock patch method could not pass env context
available = self.website.is_pricelist_available(christmas_pl)
if result:
self.assertTrue(available, 'AssertTrue failed for %s' % country)
else:
self.assertFalse(available, 'AssertFalse failed for %s' % country)
def test_get_pricelist_available_show_with_auto_property(self):
show = True
self.env.user.partner_id.country_id = self.env.ref('base.be') # Add EUR pricelist auto
current_pl = False
country_list = {
False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
'BE': ['EUR', 'Benelux'],
'IT': ['EUR'],
'CA': ['EUR', 'Canada'],
'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
}
for country, result in country_list.items():
pls = self.get_pl(show, current_pl, country)
self.assertEqual(len(set(pls.mapped('name')) & set(result)), len(pls), 'Test failed for %s (%s %s vs %s %s)'
% (country, len(pls), pls.mapped('name'), len(result), result))
def test_pricelist_combination(self):
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, {
'applied_on': '1_product',
'product_tmpl_id': product.product_tmpl_id.id,
'min_quantity': 500,
'compute_price': 'percentage',
'percent_price': 63,
})]
})
promo_pricelist = self.env['product.pricelist'].create({
'name': 'Super Pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'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
})]
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [(0, 0, {
'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,
})]
})
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')
self.assertEqual(sol.price_total, 13875)
def test_pricelist_with_no_list_price(self):
product = self.env['product.product'].create({
'name': 'Super Product',
'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,
})]
})
so = self.env['sale.order'].create({
'partner_id': self.env.user.partner_id.id,
'order_line': [(0, 0, {
'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,
})]
})
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)
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.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.
"""
pricelist = self.env['product.pricelist'].create({
'name': 'Pricelist base on cost',
'item_ids': [Command.create({
'base': 'standard_price',
'compute_price': 'percentage',
'percent_price': 10,
})]
})
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})
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)
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)
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)
def test_pricelist_item_validity_period(self):
""" Test that if a cart was created before a validity period,
the correct prices will still apply.
"""
today = datetime.today()
tomorrow = today + timedelta(days=1)
pricelist = self.env['product.pricelist'].create({
'name': 'Pricelist with validity period',
'item_ids': [Command.create({
'compute_price': 'formula',
'base': 'list_price',
'price_discount': 20,
'date_start': tomorrow,
})]
})
product = self.env['product.product'].create({
'name': 'Super Product',
'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,
'pricelist_id': pricelist.id,
'order_line': [(0, 0, {
'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,
})],
'website_id': current_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)
self.assertEqual(sol.price_unit, 80.0, 'Reduction should be applied')
self.assertEqual(sol.price_total, 160)
def simulate_frontend_context(self, website_id=1):
# Mock this method will be enough to simulate frontend context in most methods
def get_request_website():
return self.env['website'].browse(website_id)
patcher = patch('odoo.addons.website.models.ir_http.get_request_website', wraps=get_request_website)
self.startPatcher(patcher)
@tagged('post_install', '-at_install')
class TestWebsitePriceListAvailable(TransactionCase):
def setUp(self):
super(TestWebsitePriceListAvailable, self).setUp()
Pricelist = self.env['product.pricelist']
Website = self.env['website']
# Set up 2 websites
self.website = Website.browse(1)
self.website2 = Website.create({'name': 'Website 2'})
# Remove existing pricelists and create new ones
existing_pricelists = Pricelist.search([])
self.backend_pl = Pricelist.create({
'name': 'Backend Pricelist',
'website_id': False,
})
self.generic_pl_select = Pricelist.create({
'name': 'Generic Selectable Pricelist',
'selectable': True,
'website_id': False,
})
self.generic_pl_code = Pricelist.create({
'name': 'Generic Code Pricelist',
'code': 'GENERICCODE',
'website_id': False,
})
self.generic_pl_code_select = Pricelist.create({
'name': 'Generic Code Selectable Pricelist',
'code': 'GENERICCODESELECT',
'selectable': True,
'website_id': False,
})
self.w1_pl = Pricelist.create({
'name': 'Website 1 Pricelist',
'website_id': self.website.id,
})
self.w1_pl_select = Pricelist.create({
'name': 'Website 1 Pricelist Selectable',
'website_id': self.website.id,
'selectable': True,
})
self.w1_pl_code_select = Pricelist.create({
'name': 'Website 1 Pricelist Code Selectable',
'website_id': self.website.id,
'code': 'W1CODESELECT',
'selectable': True,
})
self.w1_pl_code = Pricelist.create({
'name': 'Website 1 Pricelist Code',
'website_id': self.website.id,
'code': 'W1CODE',
})
self.w2_pl = Pricelist.create({
'name': 'Website 2 Pricelist',
'website_id': self.website2.id,
})
existing_pricelists.write({'active': False})
self.website = self.env['website'].browse(1)
simulate_frontend_context(self)
def test_get_pricelist_available(self):
# all_pl = self.backend_pl + self.generic_pl_select + self.generic_pl_code + self.generic_pl_code_select + self.w1_pl + self.w1_pl_select + self.w1_pl_code + self.w1_pl_code_select + self.w2_pl
# Test get all available pricelists
pls_to_return = self.generic_pl_select + self.generic_pl_code + self.generic_pl_code_select + self.w1_pl + self.w1_pl_select + self.w1_pl_code + self.w1_pl_code_select
pls = self.website.get_pricelist_available()
self.assertEqual(pls, pls_to_return, "Every pricelist having the correct website_id set or (no website_id but a code or selectable) should be returned")
# Test get all available and visible pricelists
pls_to_return = self.generic_pl_select + self.generic_pl_code_select + self.w1_pl_select + self.w1_pl_code_select
pls = self.website.get_pricelist_available(show_visible=True)
self.assertEqual(pls, pls_to_return, "Only selectable pricelists website compliant (website_id False or current website) should be returned")
def test_property_product_pricelist_for_inactive_partner(self):
# `_get_partner_pricelist_multi` should consider inactive users when searching for pricelists.
# Real case if for public user. His `property_product_pricelist` need to be set as it is passed
# through `_get_pl_partner_order` as the `website_pl` when searching for available pricelists
# for active users.
public_partner = self.env.ref('base.public_partner')
self.assertFalse(public_partner.active, "Ensure public partner is inactive (purpose of this test)")
pl = public_partner.property_product_pricelist
self.assertEqual(len(pl), 1, "Inactive partner should still get a `property_product_pricelist`")
@tagged('post_install', '-at_install')
class TestWebsitePriceListAvailableGeoIP(TestWebsitePriceListAvailable):
def setUp(self):
super(TestWebsitePriceListAvailableGeoIP, self).setUp()
# clean `property_product_pricelist` for partner for this test (clean setup)
self.env['ir.property'].search([('res_id', '=', 'res.partner,%s' % self.env.user.partner_id.id)]).unlink()
# set different country groups on pricelists
c_EUR = self.env.ref('base.europe')
c_BENELUX = self.env['res.country.group'].create({
'name': 'BeNeLux',
'country_ids': [(6, 0, (self.env.ref('base.be') + self.env.ref('base.lu') + self.env.ref('base.nl')).ids)]
})
self.BE = self.env.ref('base.be')
NL = self.env.ref('base.nl')
c_BE = self.env['res.country.group'].create({'name': 'Belgium', 'country_ids': [(6, 0, [self.BE.id])]})
c_NL = self.env['res.country.group'].create({'name': 'Netherlands', 'country_ids': [(6, 0, [NL.id])]})
(self.backend_pl + self.generic_pl_select + self.generic_pl_code + self.w1_pl_select).write({'country_group_ids': [(6, 0, [c_BE.id])]})
(self.generic_pl_code_select + self.w1_pl + self.w2_pl).write({'country_group_ids': [(6, 0, [c_BENELUX.id])]})
(self.w1_pl_code).write({'country_group_ids': [(6, 0, [c_EUR.id])]})
(self.w1_pl_code_select).write({'country_group_ids': [(6, 0, [c_NL.id])]})
# pricelist | selectable | website | code | country group |
# ----------------------------------------------------------------------|
# backend_pl | | | | BE |
# generic_pl_select | V | | | BE |
# generic_pl_code | | | V | BE |
# generic_pl_code_select | V | | V | BENELUX |
# w1_pl | | 1 | | BENELUX |
# w1_pl_select | V | 1 | | BE |
# w1_pl_code_select | V | 1 | V | NL |
# w1_pl_code | | 1 | V | EUR |
# w2_pl | | 2 | | BENELUX |
# available pl for website 1 for GeoIP BE (anything except website 2, backend and NL)
self.website1_be_pl = self.generic_pl_select + self.generic_pl_code + self.w1_pl_select + self.generic_pl_code_select + self.w1_pl + self.w1_pl_code
def test_get_pricelist_available_geoip(self):
# Test get all available pricelists with geoip and no partner pricelist (ir.property)
# property_product_pricelist will also be returned in the available pricelists
self.website1_be_pl += self.env.user.partner_id.property_product_pricelist
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, and the partner pl")
def test_get_pricelist_available_geoip2(self):
# Test get all available pricelists with geoip and a partner pricelist (ir.property) not website compliant
self.env.user.partner_id.property_product_pricelist = self.backend_pl
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)
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()
self.assertEqual(pls, self.website1_be_pl, "Only pricelists for BE and accessible on website should be returned, but not the partner pricelist as it is website compliant but not GeoIP compliant.")
def test_get_pricelist_available_geoip4(self):
# Test get all available with geoip and visible pricelists + promo pl
pls_to_return = self.generic_pl_select + self.w1_pl_select + self.generic_pl_code_select
# property_product_pricelist will also be returned in the available pricelists
pls_to_return += self.env.user.partner_id.property_product_pricelist
current_pl = self.w1_pl_code
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):
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")
@tagged('post_install', '-at_install')
class TestWebsitePriceListHttp(HttpCaseWithUserPortal):
def test_get_pricelist_available_multi_company(self):
''' Test that the `property_product_pricelist` of `res.partner` is not
computed as SUPERUSER_ID.
Indeed, `property_product_pricelist` is a _compute that ends up
doing a search on `product.pricelist` that woule bypass the
pricelist multi-company `ir.rule`. Then it would return pricelists
from another company and the code would raise an access error when
reading that `property_product_pricelist`.
'''
test_company = self.env['res.company'].create({'name': 'Test Company'})
test_company.flush_recordset()
self.env['product.pricelist'].create({
'name': 'Backend Pricelist For "Test Company"',
'website_id': False,
'company_id': test_company.id,
'sequence': 1,
})
self.authenticate('portal', 'portal')
r = self.url_open('/shop')
self.assertEqual(r.status_code, 200, "The page should not raise an access error because of reading pricelists from other companies")
@tagged('post_install', '-at_install')
class TestWebsitePriceListMultiCompany(TransactionCaseWithUserDemo):
def setUp(self):
''' Create a basic multi-company pricelist environment:
- Set up 2 companies with their own company-restricted pricelist each.
- Add demo user in those 2 companies
- For each company, add that company pricelist to the demo user partner.
- Set website's company to company 2
- Demo user will still be in company 1
'''
super(TestWebsitePriceListMultiCompany, self).setUp()
self.demo_user = self.user_demo
# Create and add demo user to 2 companies
self.company1 = self.demo_user.company_id
self.company2 = self.env['res.company'].create({'name': 'Test Company'})
self.demo_user.company_ids += self.company2
# Set company2 as current company for demo user
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,
})
# Create a company pricelist for each company and set it to demo user
self.c1_pl = self.env['product.pricelist'].create({
'name': 'Company 1 Pricelist',
'company_id': self.company1.id,
# The `website_id` field will default to the company's website,
# in this case `self.website2`.
})
self.c2_pl = self.env['product.pricelist'].create({
'name': 'Company 2 Pricelist',
'company_id': self.company2.id,
'website_id': False,
})
self.demo_user.partner_id.with_company(self.company1.id).property_product_pricelist = self.c1_pl
self.demo_user.partner_id.with_company(self.company2.id).property_product_pricelist = self.c2_pl
# Ensure everything was done correctly
self.assertEqual(self.demo_user.partner_id.with_company(self.company1.id).property_product_pricelist, self.c1_pl)
self.assertEqual(self.demo_user.partner_id.with_company(self.company2.id).property_product_pricelist, self.c2_pl)
irp1 = self.env['ir.property'].with_company(self.company1)._get("property_product_pricelist", "res.partner", self.demo_user.partner_id.id)
irp2 = self.env['ir.property'].with_company(self.company2)._get("property_product_pricelist", "res.partner", self.demo_user.partner_id.id)
self.assertEqual((irp1, irp2), (self.c1_pl, self.c2_pl), "Ensure there is an `ir.property` for demo partner for every company, and that the pricelist is the company specific one.")
# ---------------------------------- IR.PROPERTY -------------------------------------
# id | name | res_id | company_id | value_reference
# ------------------------------------------------------------------------------------
# 1 | 'property_product_pricelist' | | 1 | product.pricelist,1
# 2 | 'property_product_pricelist' | | 2 | product.pricelist,2
# 3 | 'property_product_pricelist' | res.partner,8 | 1 | product.pricelist,10
# 4 | 'property_product_pricelist' | res.partner,8 | 2 | product.pricelist,11
def test_property_product_pricelist_multi_company(self):
''' Test that the `property_product_pricelist` of `res.partner` is read
for the company of the website and not the current user company.
This is the case if the user visit a website for which the company
is not the same as its user's company.
Here, as demo user (company1), we will visit website1 (company2).
It should return the ir.property for demo user for company2 and not
for the company1 as we should get the website's company pricelist
and not the demo user's current company pricelist.
'''
simulate_frontend_context(self, self.website.id)
# First check: It should return ir.property,4 as company_id is
# website.company_id and not env.user.company_id
company_id = self.website.company_id.id
partner = self.demo_user.partner_id.with_company(company_id)
demo_pl = partner.property_product_pricelist
self.assertEqual(demo_pl, self.c2_pl)
# Second thing to check: It should not error in read right access error
# Indeed, the ir.rule for pricelists rights about company should allow to
# also read a pricelist from another company if that company is the one
# from the currently visited website.
self.env(user=self.user_demo)['product.pricelist'].browse(demo_pl.id).name
def test_archive_pricelist_1(self):
''' Test that when a pricelist is archived, the check that verify that
all website have at least one pricelist have access to all
pricelists (considering all companies).
'''
self.c2_pl.website_id = self.website
c2_pl2 = self.c2_pl.copy({'name': 'Copy of c2_pl'})
self.env['product.pricelist'].search([
('id', 'not in', (self.c2_pl + self.c1_pl + c2_pl2).ids)
]).write({'active': False})
# ---------------- PRICELISTS ----------------
# name | website_id | company_id |
# --------------------------------------------
# self.c1_pl | self.website2 | self.company1 |
# 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')
# The test is here: while having access only to self.company2 records,
# archive should not raise an error
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):
def test_update_pricelist_user_session(self):
"""
The objective is to verify that the pricelist
changes correctly according to the user.
"""
website = self.env.ref('website.default_website')
test_user = self.env['res.users'].create({
'name': 'Toto',
'login': 'toto',
'password': 'long_enough_password',
})
# We need at least two selectable pricelists to display the dropdown
self.env['product.pricelist'].create([{
'name': 'Public Pricelist 1',
'selectable': True
}, {
'name': 'Public Pricelist 2',
'selectable': True
}])
user_pricelist = self.env['product.pricelist'].create({
'name': 'User Pricelist',
'website_id': website.id,
'code': 'User_pricelist',
})
test_user.partner_id.property_product_pricelist = user_pricelist
self.start_tour("/shop", 'website_sale_shop_pricelist_tour', login="")

View file

@ -0,0 +1,144 @@
# 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

@ -0,0 +1,317 @@
# -*- 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.tests import tagged
from odoo.addons.website.tools import MockRequest
@tagged('post_install', '-at_install', 'product_attribute')
class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# 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)
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 product with 15% tax.
product_template = self.computer.with_context(website_id=website.id)
product_template.write({
'taxes_id': [Command.set(self.company_data['default_tax_sale'].ids)],
'company_id': self.env.company.id,
})
tax_ratio = 1.15
discount_rate = 0.9
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)
# 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
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)
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,
'taxes_id': [Command.set(self.company_data['default_tax_sale'].ids)],
'company_id': self.env.company.id,
})
# Setup pricelist: make sure the pricelist has a 10% discount
pricelist = self.env['product.pricelist'].create({
'name': "test_get_combination_info",
'company_id': self.env.company.id,
'item_ids': [Command.create({
'applied_on': "1_product",
'base': "list_price",
'compute_price': "fixed",
'fixed_price': 500,
'product_tmpl_id': product.id,
})],
})
# 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.
computer_ssd_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': product.id,
'attribute_id': self.ssd_attribute.id,
'value_ids': [(6, 0, [self.ssd_256.id])],
})
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
combination_info = product._get_combination_info(pricelist=pricelist)
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({
'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,
})],
})
# 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.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
# Reset / Safety check
self.env.user.partner_id.country_id = None
combination_info = product._get_combination_info(pricelist=pricelist)
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.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)
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)")
@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,
})
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,
})
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%)")

View file

@ -0,0 +1,86 @@
# -*- 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
@tagged('post_install', '-at_install')
class TestWebsiteSaleReorderFromPortal(HttpCase):
@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([
{
'name': 'Reorder Product 1',
'sale_ok': True,
'website_published': True,
},
{
'name': 'Reorder Product 2',
'sale_ok': True,
'website_published': True,
},
])
no_variant_attribute = self.env['product.attribute'].create({
'name': 'Size',
'create_variant': 'no_variant',
'value_ids': [
Command.create({'name': 'S'}),
Command.create({'name': 'M'}),
Command.create({'name': 'L', 'is_custom': True}),
]
})
s, _m, l = no_variant_attribute.value_ids
no_variant_template = self.env['product.template'].create({
'name': 'Sofa',
'attribute_line_ids': [Command.create({
'attribute_id': no_variant_attribute.id,
'value_ids': [Command.set(no_variant_attribute.value_ids.ids)],
})]
})
ptavs = no_variant_template.attribute_line_ids.product_template_value_ids
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({
'partner_id': user_admin.partner_id.id,
'state': 'sale',
'order_line': [
Command.create({
'product_id': product_1.id,
}),
Command.create({
'product_id': product_2.id,
}),
Command.create({
'product_id': no_variant_template.product_variant_id.id,
'product_no_variant_attribute_value_ids': [Command.set(ptav_s.ids)],
}),
Command.create({
'product_id': no_variant_template.product_variant_id.id,
'product_no_variant_attribute_value_ids': [Command.set(ptav_l.ids)],
'product_custom_attribute_value_ids': [Command.create({
'custom_product_template_attribute_value_id': ptav_l.id,
'custom_value': 'Whatever',
})]
})
],
})
order.message_subscribe(user_admin.partner_id.ids)
self.start_tour("/", 'website_sale_reorder_from_portal', login='admin')
reorder_cart = self.env['sale.order'].search([('website_id', '!=', False)], limit=1)
previous_lines = order.order_line
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_no_variant_attribute_value_ids,
order_lines.product_no_variant_attribute_value_ids,
)

View file

@ -0,0 +1,145 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
@tagged('post_install', '-at_install')
class WebsiteSaleShopPriceListCompareListPriceDispayTests(AccountTestInvoicingHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
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",
'company_id': cls.env.company.id,
'sequence': 1,
})
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,
'company_id': cls.env.company.id,
})
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({
'name': 'pricelist_default',
'website_id': website.id,
'company_id': cls.env.company.id,
'selectable': True,
'sequence': 1,
})
cls.pricelist_with_discount = Pricelist.create({
'name': 'pricelist_with_discount',
'website_id': website.id,
'company_id': cls.env.company.id,
'selectable': True,
'sequence': 2,
'discount_policy': 'with_discount',
})
cls.pricelist_without_discount = Pricelist.create({
'name': 'pricelist_without_discount',
'website_id': website.id,
'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,
})
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.start_tour("/", 'compare_list_price_price_list_display', login=self.env.user.login)

View file

@ -0,0 +1,59 @@
# -*- 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
_logger = logging.getLogger(__name__)
@tagged('post_install', '-at_install', 'website_snippets')
class TestSnippets(HttpCase):
def test_01_snippet_products_edition(self):
self.env['product.product'].create({
'name': 'Test Product',
'website_published': True,
'sale_ok': True,
'list_price': 500,
})
self.env['product.product'].create({
'name': 'Test Product 2',
'website_published': True,
'sale_ok': True,
'list_price': 500,
})
self.env['product.product'].create({
'name': 'Test Product 3',
'website_published': True,
'sale_ok': True,
'list_price': 500,
})
self.env['product.product'].create({
'name': 'Test Product 4',
'website_published': True,
'sale_ok': True,
'list_price': 500,
})
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")

View file

@ -0,0 +1,119 @@
# coding: utf-8
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website.tools import MockRequest
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class WebsiteSaleVisitorTests(TransactionCase):
def setUp(self):
super().setUp()
self.website = self.env.ref('website.default_website')
self.WebsiteSaleController = WebsiteSale()
self.cookies = {}
def test_create_visitor_on_tracked_product(self):
self.WebsiteSaleController = WebsiteSale()
existing_visitors = self.env['website.visitor'].search([])
existing_tracks = self.env['website.track'].search([])
product = self.env['product.product'].create({
'name': 'Storage Box',
'website_published': True,
})
with MockRequest(self.env, website=self.website):
self.cookies = self.WebsiteSaleController.products_recently_viewed_update(product.id)
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
self.assertEqual(len(new_visitors), 1, "A visitor should be created after visiting a tracked product")
self.assertEqual(len(new_tracks), 1, "A track should be created after visiting a tracked product")
with MockRequest(self.env, website=self.website, cookies=self.cookies):
self.WebsiteSaleController.products_recently_viewed_update(product.id)
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
self.assertEqual(len(new_visitors), 1, "No visitor should be created after visiting another tracked product")
self.assertEqual(len(new_tracks), 1, "No track should be created after visiting the same tracked product before 30 min")
product = self.env['product.product'].create({
'name': 'Large Cabinet',
'website_published': True,
'list_price': 320.0,
})
with MockRequest(self.env, website=self.website, cookies=self.cookies):
self.WebsiteSaleController.products_recently_viewed_update(product.id)
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
new_tracks = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
self.assertEqual(len(new_visitors), 1, "No visitor should be created after visiting another tracked product")
self.assertEqual(len(new_tracks), 2, "A track should be created after visiting another tracked product")
def test_dynamic_filter_newest_products(self):
"""Test that a product is not displayed anymore after
changing it company."""
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',
'website_published': True,
'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')
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, [])
res_products = [res_product['_record'] for res_product in res]
self.assertNotIn(product, res_products)
def test_recently_viewed_company_changed(self):
"""Test that a product is :
- displayed after visiting it
- not displayed after changing it company."""
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',
'website_published': True,
'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_latest_viewed_products')
# BEFORE VISITING THE PRODUCT
res = snippet_filter._prepare_values(16, [])
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, [])
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, [])
self.assertFalse(res)

View file

@ -0,0 +1,76 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
@odoo.tests.common.tagged('post_install', '-at_install')
class TestWebsiteSequence(odoo.tests.TransactionCase):
def setUp(self):
super(TestWebsiteSequence, self).setUp()
ProductTemplate = self.env['product.template']
product_templates = ProductTemplate.search([])
# if stock is installed we can't archive since there is orderpoints
if hasattr(self.env['product.product'], 'orderpoint_ids'):
product_templates.mapped('product_variant_ids.orderpoint_ids').write({'active': False})
# if pos loyalty is installed we can't archive since there are loyalty rules and rewards
if 'loyalty.program' in self.env:
programs = self.env['loyalty.program'].search([])
programs.active = False
programs.coupon_ids.unlink()
programs.unlink()
product_templates.write({'active': False})
self.p1, self.p2, self.p3, self.p4 = ProductTemplate.create([{
'name': 'First Product',
'website_sequence': 100,
}, {
'name': 'Second Product',
'website_sequence': 180,
}, {
'name': 'Third Product',
'website_sequence': 225,
}, {
'name': 'Last Product',
'website_sequence': 250,
}])
self._check_correct_order(self.p1 + self.p2 + self.p3 + self.p4)
def _search_website_sequence_order(self, order='ASC'):
'''Helper method to limit the search only to the setUp products'''
return self.env['product.template'].search([
], order='website_sequence %s' % (order))
def _check_correct_order(self, products):
product_ids = self._search_website_sequence_order().ids
self.assertEqual(product_ids, products.ids, "Wrong sequence order")
def test_01_website_sequence(self):
# 100:1, 180:2, 225:3, 250:4
self.p2.set_sequence_down()
# 100:1, 180:3, 225:2, 250:4
self._check_correct_order(self.p1 + self.p3 + self.p2 + self.p4)
self.p4.set_sequence_up()
# 100:1, 180:3, 225:4, 250:2
self._check_correct_order(self.p1 + self.p3 + self.p4 + self.p2)
self.p2.set_sequence_top()
# 95:2, 100:1, 180:3, 225:4
self._check_correct_order(self.p2 + self.p1 + self.p3 + self.p4)
self.p1.set_sequence_bottom()
# 95:2, 180:3, 225:4, 230:1
self._check_correct_order(self.p2 + self.p3 + self.p4 + self.p1)
current_sequences = self._search_website_sequence_order().mapped('website_sequence')
self.assertEqual(current_sequences, [95, 180, 225, 230], "Wrong sequence order (2)")
self.p2.website_sequence = 1
self.p3.set_sequence_top()
# -4:3, 1:2, 225:4, 230:1
self.assertEqual(self.p3.website_sequence, -4, "`website_sequence` should go below 0")
new_product = self.env['product.template'].create({
'name': 'Last Newly Created Product',
})
self.assertEqual(self._search_website_sequence_order()[-1], new_product, "new product should be last")