# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields from odoo.addons.product.tests.test_product_attribute_value_config import TestProductAttributeValueCommon from odoo.tests import tagged class TestSaleProductAttributeValueCommon(TestProductAttributeValueCommon): @classmethod def _setup_currency(cls, currency_ratio=2): """Get or create a currency. This makes the test non-reliant on demo. With an easy currency rate, for a simple 2 ratio in the following tests. """ from_currency = cls.computer.currency_id cls._set_or_create_rate_today(from_currency, rate=1) to_currency = cls._get_or_create_currency("my currency", "C") cls._set_or_create_rate_today(to_currency, currency_ratio) return to_currency @classmethod def _set_or_create_rate_today(cls, currency, rate): """Get or create a currency rate for today. This makes the test non-reliant on demo data.""" name = fields.Date.today() currency_id = currency.id company_id = cls.env.company.id CurrencyRate = cls.env['res.currency.rate'] currency_rate = CurrencyRate.search([ ('company_id', '=', company_id), ('currency_id', '=', currency_id), ('name', '=', name), ]) if currency_rate: currency_rate.rate = rate else: CurrencyRate.create({ 'company_id': company_id, 'currency_id': currency_id, 'name': name, 'rate': rate, }) @classmethod def _get_or_create_currency(cls, name, symbol): """Get or create a currency based on name. This makes the test non-reliant on demo data.""" currency = cls.env['res.currency'].search([('name', '=', name)]) return currency or currency.create({ 'name': name, 'symbol': symbol, }) @tagged('post_install', '-at_install') class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon): def _setup_pricelist(self, currency_ratio=2): to_currency = self._setup_currency(currency_ratio) discount = 10 pricelist = self.env['product.pricelist'].create({ 'name': 'test pl', 'currency_id': to_currency.id, 'company_id': self.computer.company_id.id, }) pricelist_item = self.env['product.pricelist.item'].create({ 'min_quantity': 2, 'compute_price': 'percentage', 'percent_price': discount, 'pricelist_id': pricelist.id, }) return (pricelist, pricelist_item, currency_ratio, 1 - discount / 100) def test_01_is_combination_possible_archived(self): """The goal is to test the possibility of archived combinations. This test could not be put into product module because there was no model which had product_id as required and without cascade on delete. Here we have the sales order line in this situation. This is a necessary condition for `_create_variant_ids` to archive instead of delete the variants. """ def do_test(self): computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2) variant = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1) variant2 = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_2) self.assertTrue(variant) self.assertTrue(variant2) # Create a dummy SO to prevent the variant from being deleted by # _create_variant_ids() because the variant is a related field that # is required on the SO line so = self.env['sale.order'].create({'partner_id': 1}) self.env['sale.order.line'].create({ 'order_id': so.id, 'name': "test", 'product_id': variant.id }) # additional variant to test correct ignoring when mismatch values self.env['sale.order.line'].create({ 'order_id': so.id, 'name': "test", 'product_id': variant2.id }) variant2.active = False # CASE: 1 not archived, 2 archived self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_2)) # CASE: both archived combination (without no_variant) variant.active = False self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_2)) self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) # CASE: OK after attribute line removed self.computer_hdd_attribute_lines.write({'active': False}) self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8)) # CASE: not archived (with no_variant) self.hdd_attribute.create_variant = 'no_variant' self._add_hdd_attribute_line() computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2) self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) # CASE: archived combination found (with no_variant) variant = self.computer._get_variant_for_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_1) variant.active = False self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) # CASE: archived combination has different attributes (including no_variant) self.computer_ssd_attribute_lines.write({'active': False}) variant4 = self.computer._get_variant_for_combination(computer_ram_8 + computer_hdd_1) self.env['sale.order.line'].create({ 'order_id': so.id, 'name': "test", 'product_id': variant4.id }) self.assertTrue(self.computer._is_combination_possible(computer_ram_8 + computer_hdd_1)) # CASE: archived combination has different attributes (without no_variant) self.computer_hdd_attribute_lines.write({'active': False}) self.hdd_attribute.create_variant = 'always' self._add_hdd_attribute_line() computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) computer_hdd_2 = self._get_product_template_attribute_value(self.hdd_2) variant5 = self.computer._get_variant_for_combination(computer_ram_8 + computer_hdd_1) self.env['sale.order.line'].create({ 'order_id': so.id, 'name': "test", 'product_id': variant5.id }) self.assertTrue(variant4 != variant5) self.assertTrue(self.computer._is_combination_possible(computer_ram_8 + computer_hdd_1)) computer_ssd_256_before = self._get_product_template_attribute_value(self.ssd_256) do_test(self) # CASE: add back the removed attribute and try everything again self.computer_ssd_attribute_lines = self.env['product.template.attribute.line'].create({ 'product_tmpl_id': self.computer.id, 'attribute_id': self.ssd_attribute.id, 'value_ids': [(6, 0, [self.ssd_256.id, self.ssd_512.id])], }) computer_ssd_256_after = self._get_product_template_attribute_value(self.ssd_256) self.assertEqual(computer_ssd_256_after, computer_ssd_256_before) self.assertEqual(computer_ssd_256_after.attribute_line_id, computer_ssd_256_before.attribute_line_id) do_test(self) def test_02_get_combination_info(self): # If using multi-company, company_id will be False, and this code should # still work. # The case with a company_id will be implicitly tested on website_sale. self.computer.company_id = False computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) # CASE: no pricelist, no currency, with existing combination, with price_extra on attributes combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 computer_variant = self.computer._get_variant_for_combination(combination) res = self.computer._get_combination_info(combination) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], computer_variant.id) self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)") self.assertEqual(res['price'], 2222) self.assertEqual(res['list_price'], 2222) # CASE: no combination, product given res = self.computer._get_combination_info(self.env['product.template.attribute.value'], computer_variant.id) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], computer_variant.id) self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)") self.assertEqual(res['price'], 2222) self.assertEqual(res['list_price'], 2222) # CASE: using pricelist, quantity rule pricelist, pricelist_item, currency_ratio, discount_ratio = self._setup_pricelist() res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], computer_variant.id) self.assertEqual(res['display_name'], "Super Computer (256 GB, 8 GB, 1 To)") self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) # CASE: no_variant combination, it's another variant now self.computer_ssd_attribute_lines.write({'active': False}) self.ssd_attribute.create_variant = 'no_variant' self._add_ssd_attribute_line() computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 computer_variant_new = self.computer._get_variant_for_combination(combination) self.assertTrue(computer_variant_new) res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], computer_variant_new.id) self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To)") self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) # CASE: dynamic combination, but the variant already exists self.computer_hdd_attribute_lines.write({'active': False}) self.hdd_attribute.create_variant = 'dynamic' self._add_hdd_attribute_line() computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 computer_variant_new = self.computer._create_product_variant(combination) self.assertTrue(computer_variant_new) res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], computer_variant_new.id) self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To)") self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) # CASE: dynamic combination, no variant existing # Test invalidate_cache on product.template _create_variant_ids self._add_keyboard_attribute() combination += self._get_product_template_attribute_value(self.keyboard_excluded) res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], False) self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To, Excluded)") self.assertEqual(res['price'], (2222 - 5) * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], (2222 - 5) * currency_ratio) # CASE: pricelist set value to 0, no variant # Test invalidate_cache on product.pricelist write pricelist_item.percent_price = 100 res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['product_template_id'], self.computer.id) self.assertEqual(res['product_id'], False) self.assertEqual(res['display_name'], "Super Computer (8 GB, 1 To, Excluded)") self.assertEqual(res['price'], 0) self.assertEqual(res['list_price'], (2222 - 5) * currency_ratio) def test_03_get_combination_info_discount_policy(self): computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 pricelist, pricelist_item, currency_ratio, discount_ratio = self._setup_pricelist() pricelist.discount_policy = 'with_discount' # CASE: no discount, setting with_discount res = self.computer._get_combination_info(combination, add_qty=1, pricelist=pricelist) self.assertEqual(res['price'], 2222 * currency_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) self.assertEqual(res['has_discounted_price'], False) # CASE: discount, setting with_discount res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) self.assertEqual(res['has_discounted_price'], False) # CASE: no discount, setting without_discount pricelist.discount_policy = 'without_discount' res = self.computer._get_combination_info(combination, add_qty=1, pricelist=pricelist) self.assertEqual(res['price'], 2222 * currency_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) self.assertEqual(res['has_discounted_price'], False) # CASE: discount, setting without_discount res = self.computer._get_combination_info(combination, add_qty=2, pricelist=pricelist) self.assertEqual(res['price'], 2222 * currency_ratio * discount_ratio) self.assertEqual(res['list_price'], 2222 * currency_ratio) self.assertEqual(res['has_discounted_price'], True) def test_04_create_product_variant_non_dynamic(self): """The goal of this test is to make sure the create_product_variant does not create variant if the type is not dynamic. It can however return a variant if it already exists.""" computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) self._add_exclude(computer_ram_16, computer_hdd_1) # CASE: variant is already created, it should return it combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 variant1 = self.computer._get_variant_for_combination(combination) self.assertEqual(self.computer._create_product_variant(combination), variant1) # CASE: variant does not exist, but template is non-dynamic, so it # should not create it Product = self.env['product.product'] variant1.unlink() self.assertEqual(self.computer._create_product_variant(combination), Product) def test_05_create_product_variant_dynamic(self): """The goal of this test is to make sure the create_product_variant does work with dynamic. If the combination is possible, it should create it. If it's not possible, it should not create it.""" self.computer_hdd_attribute_lines.write({'active': False}) self.hdd_attribute.create_variant = 'dynamic' self._add_hdd_attribute_line() computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_ram_16 = self._get_product_template_attribute_value(self.ram_16) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) self._add_exclude(computer_ram_16, computer_hdd_1) # CASE: variant does not exist, but combination is not possible # so it should not create it impossible_combination = computer_ssd_256 + computer_ram_16 + computer_hdd_1 Product = self.env['product.product'] self.assertEqual(self.computer._create_product_variant(impossible_combination), Product) # CASE: the variant does not exist, and the combination is possible, so # it should create it combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1 variant = self.computer._create_product_variant(combination) self.assertTrue(variant) # CASE: the variant already exists, so it should return it self.assertEqual(variant, self.computer._create_product_variant(combination)) def _add_keyboard_attribute(self): self.keyboard_attribute = self.env['product.attribute'].create({ 'name': 'Keyboard', 'sequence': 6, 'create_variant': 'dynamic', }) self.keyboard_included = self.env['product.attribute.value'].create({ 'name': 'Included', 'attribute_id': self.keyboard_attribute.id, 'sequence': 1, }) self.keyboard_excluded = self.env['product.attribute.value'].create({ 'name': 'Excluded', 'attribute_id': self.keyboard_attribute.id, 'sequence': 2, }) self.computer_keyboard_attribute_lines = self.env['product.template.attribute.line'].create({ 'product_tmpl_id': self.computer.id, 'attribute_id': self.keyboard_attribute.id, 'value_ids': [(6, 0, [self.keyboard_included.id, self.keyboard_excluded.id])], }) self.computer_keyboard_attribute_lines.product_template_value_ids[0].price_extra = 5 self.computer_keyboard_attribute_lines.product_template_value_ids[1].price_extra = -5