diff options
37 files changed, 480 insertions, 340 deletions
diff --git a/addons/product/models/res_config_settings.py b/addons/product/models/res_config_settings.py index 7e73f4df5e6..6272b1f37ba 100644 --- a/addons/product/models/res_config_settings.py +++ b/addons/product/models/res_config_settings.py @@ -14,6 +14,7 @@ class ResConfigSettings(models.TransientModel): " * Unchecked : Each company can see only its product (product where company is defined). Product not related to a company are visible for all companies.") group_uom = fields.Boolean("Units of Measure", implied_group='uom.group_uom') group_product_variant = fields.Boolean("Variants and Options", implied_group='product.group_product_variant') + module_sale_product_configurator = fields.Boolean("Product Configurator") group_stock_packaging = fields.Boolean('Product Packagings', implied_group='product.group_stock_packaging') group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers", @@ -33,6 +34,19 @@ class ResConfigSettings(models.TransientModel): ('1', 'Cubic Feet'), ], 'Volume unit of measure', config_parameter='product.volume_in_cubic_feet', default='0') + @api.onchange('group_product_variant') + def _onchange_group_product_variant(self): + """The product Configurator requires the product variants activated. + If the user disables the product variants -> disable the product configurator as well""" + if self.module_sale_product_configurator and not self.group_product_variant: + self.module_sale_product_configurator = False + + @api.onchange('module_sale_product_configurator') + def _onchange_module_sale_product_configurator(self): + """The product Configurator requires the product variants activated + If the user enables the product configurator -> enable the product variants as well""" + if self.module_sale_product_configurator and not self.group_product_variant: + self.group_product_variant = True @api.model def get_values(self): diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index 741d357c900..c275df0ac9b 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -27,13 +27,12 @@ This module contains all the common features of Sales Management and eCommerce. 'views/res_partner_views.xml', 'views/mail_activity_views.xml', 'views/assets.xml', + 'views/variant_templates.xml', 'views/sale_portal_templates.xml', - 'views/sale_product_configurator_templates.xml', 'views/sale_onboarding_views.xml', 'views/res_config_settings_views.xml', 'views/payment_views.xml', 'views/product_attribute_views.xml', - 'wizard/sale_product_configurator_views.xml', ], 'demo': [ 'data/sale_demo.xml', diff --git a/addons/sale/controllers/__init__.py b/addons/sale/controllers/__init__.py index ac5f1269f82..a35a880227c 100644 --- a/addons/sale/controllers/__init__.py +++ b/addons/sale/controllers/__init__.py @@ -3,4 +3,4 @@ from . import onboarding from . import portal -from . import product_configurator +from . import variant diff --git a/addons/sale/controllers/variant.py b/addons/sale/controllers/variant.py new file mode 100644 index 00000000000..3ca2c7c32b0 --- /dev/null +++ b/addons/sale/controllers/variant.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request + + +class VariantController(http.Controller): + @http.route(['/sale/get_combination_info'], type='json', auth="user", methods=['POST']) + def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw): + combination = request.env['product.template.attribute.value'].browse(combination) + pricelist = self._get_pricelist(pricelist_id) + ProductTemplate = request.env['product.template'] + if 'context' in kw: + ProductTemplate = ProductTemplate.with_context(**kw.get('context')) + return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + + @http.route(['/sale/create_product_variant'], type='json', auth="user", methods=['POST']) + def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): + return request.env['product.template'].browse(int(product_template_id)).create_product_variant(product_template_attribute_value_ids) + + def _get_pricelist(self, pricelist_id, pricelist_fallback=False): + return request.env['product.pricelist'].browse(int(pricelist_id or 0)) + + def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw): + """deprecated, use product method""" + combination = request.env['product.template.attribute.value'].browse(combination) + return request.env['product.template'].browse(product_template_id)._get_combination_info(combination, product_id, add_qty, pricelist) diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml index d0ccf291eff..d83063b9158 100644 --- a/addons/sale/data/sale_demo.xml +++ b/addons/sale/data/sale_demo.xml @@ -730,23 +730,6 @@ Thanks!</field> <field name="confirmation_date" eval="datetime.now() - relativedelta(months=1)"/> </record> - <record id="product_product_1_product_template" model="product.template"> - <field name="name">Chair floor protection</field> - <field name="categ_id" ref="product.product_category_5"/> - <field name="list_price">12.0</field> - <field name="weight">0.01</field> - <field name="uom_id" ref="uom.product_uom_unit"/> - <field name="uom_po_id" ref="uom.product_uom_unit"/> - <field name="description_sale">Office chairs can harm your floor: protect it.</field> - <field name="image" type="base64" file="sale/static/img/floor_protection-image.png"/> - </record> - <record id="product.product_product_4_product_template" model="product.template"> - <field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template')])]"/> - </record> - <record id="product.product_product_11_product_template" model="product.template"> - <field name="optional_product_ids" eval="[(6,0,[ref('product_product_1_product_template')])]"/> - </record> - <record id="product_attribute_value_7" model="product.attribute.value"> <field name="name">Custom</field> <field name="attribute_id" ref="product.product_attribute_1"/> diff --git a/addons/sale/models/product_product.py b/addons/sale/models/product_product.py index a489b03b952..8d919e39ac3 100644 --- a/addons/sale/models/product_product.py +++ b/addons/sale/models/product_product.py @@ -5,16 +5,6 @@ from odoo import api, fields, models from odoo.tools.float_utils import float_round -class ProductTemplate(models.Model): - _inherit = "product.template" - - optional_product_ids = fields.Many2many( - 'product.template', 'product_optional_rel', 'src_id', 'dest_id', - string='Optional Products', help="Optional Products are suggested " - "whenever the customer hits *Add to Cart* (cross-sell strategy, " - "e.g. for computers: warranty, software, etc.).") - - class ProductProduct(models.Model): _inherit = 'product.product' diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 80471814207..7e7847215a7 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -68,7 +68,7 @@ var ProductConfiguratorMixin = { self._checkExclusions($parent, combination); - ajax.jsonRpc(this._getUri('/product_configurator/get_combination_info'), 'call', { + ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', { product_template_id: parseInt($parent.find('.product_template_id').val()), product_id: this._getProductId($parent), combination: combination, @@ -311,7 +311,7 @@ var ProductConfiguratorMixin = { // We don't handle that compatibility because the previous code was // not working either: it was making an RPC that failed with any // non-admin user anyway. To use this feature, restart the server. - var route = '/product_configurator/create_product_variant'; + var route = '/sale/create_product_variant'; if (useAjax) { productReady = ajax.jsonRpc(route, 'call', params); } else { diff --git a/addons/sale/views/assets.xml b/addons/sale/views/assets.xml index ea5c82b1c86..9e7e42eff80 100644 --- a/addons/sale/views/assets.xml +++ b/addons/sale/views/assets.xml @@ -8,11 +8,6 @@ <xpath expr="script[last()]" position="after"> <script type="text/javascript" src="/sale/static/src/js/sale.js"></script> <script type="text/javascript" src="/sale/static/src/js/tour.js"></script> - <script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script> - <script type="text/javascript" src="/sale/static/src/js/product_configurator_renderer.js"></script> - <script type="text/javascript" src="/sale/static/src/js/product_configurator_controller.js"></script> - <script type="text/javascript" src="/sale/static/src/js/product_configurator_view.js"></script> - <script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script> <script type="text/javascript" src="/sale/static/src/js/account_reconciliation.js"></script> </xpath> </template> @@ -28,7 +23,6 @@ <template id="qunit_suite" name="sale_tests" inherit_id="web.qunit_suite"> <xpath expr="//t[@t-set='head']" position="inside"> <script type="text/javascript" src="/sale/static/tests/sales_team_dashboard_tests.js"></script> - <script type="text/javascript" src="/sale/static/tests/product_configurator.test.js"></script> </xpath> </template> </odoo> diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index bc1366e5d5c..3f2bb403453 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -18,7 +18,7 @@ <div class="o_setting_right_pane"> <label for="group_product_variant"/> <div class="text-muted"> - Configure your products with variants and select optional products + Set product attributes (e.g. color, size) to sell variants </div> <div class="content-group" attrs="{'invisible': [('group_product_variant','=',False)]}"> <div class="mt16"> @@ -29,6 +29,17 @@ </div> <div class="col-12 col-lg-6 o_setting_box"> <div class="o_setting_left_pane"> + <field name="module_sale_product_configurator"/> + </div> + <div class="o_setting_right_pane"> + <label for="module_sale_product_configurator"/> + <div class="text-muted"> + Configure your attributes with exclusions and select optional products + </div> + </div> + </div> + <div class="col-12 col-lg-6 o_setting_box"> + <div class="o_setting_left_pane"> <field name="group_uom"/> </div> <div class="o_setting_right_pane"> diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index bcf728e356b..e793650ecb6 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -81,13 +81,6 @@ <xpath expr="//page[@name='sales']" position="attributes"> <attribute name="invisible">0</attribute> </xpath> - <xpath expr="//group[@name='sale']" position="after"> - <group name="options" groups="product.group_product_variant"> - <group string="Options"> - <field name="optional_product_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('id', '!=', active_id)]" /> - </group> - </group> - </xpath> </field> </record> diff --git a/addons/sale/views/variant_templates.xml b/addons/sale/views/variant_templates.xml new file mode 100644 index 00000000000..8d52d860be4 --- /dev/null +++ b/addons/sale/views/variant_templates.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="variants"> + <t t-set="attribute_exclusions" t-value="product._get_attribute_exclusions(parent_combination, parent_name)"/> + <ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)"> + <t t-foreach="product._get_valid_product_template_attribute_lines()" t-as="variant_id"> + <!-- Attributes selection is hidden if there is only one value available and it's not a custom value --> + <li t-att-data-attribute_id="variant_id.attribute_id.id" + t-att-data-attribute_name="variant_id.attribute_id.name" + t-attf-class="variant_attribute #{'d-none' if len(variant_id.product_template_value_ids) == 1 and not variant_id.product_template_value_ids[0].is_custom else ''}"> + + <strong t-field="variant_id.attribute_id.name" class="attribute_name"/> + + <t t-if="variant_id.attribute_id.type == 'select'"> + <select + t-att-data-attribute_id="variant_id.attribute_id.id" + t-attf-class="form-control js_variant_change #{variant_id.attribute_id.create_variant}" + t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"> + <t t-foreach="variant_id.product_template_value_ids" t-as="value_id"> + <option t-att-value="value_id.id" + t-att-data-value_id="value_id.product_attribute_value_id.id" + t-att-data-value_name="value_id.name" + t-att-data-attribute_name="value_id.attribute_id.name" + t-att-data-is_custom="value_id.is_custom" + t-att-selected="first_possible_combination[variant_id_index] == value_id"> + <span t-field="value_id.name"/> + <span t-if="value_id.price_extra" class="badge badge-pill badge-secondary"> + <!-- + price_extra is displayed as catalog price instead of + price after pricelist because it is impossible to + compute. Indeed, the pricelist rule might depend on the + selected variant, so the price_extra will be different + depending on the selected combination. The price of an + attribute is therefore variable and it's not very + accurate to display it. + --> + <t t-esc="value_id.price_extra > 0 and '+' or '-'"/> + <span t-esc="abs(value_id.price_extra)" class="variant_price_extra" style="white-space: nowrap;" + t-options='{ + "widget": "monetary", + "from_currency": product.currency_id, + "display_currency": (pricelist or product).currency_id + }'/> + </span> + </option> + </t> + </select> + </t> + + <t t-if="variant_id.attribute_id.type == 'radio'"> + <ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-unstyled"> + <t t-foreach="variant_id.product_template_value_ids" t-as="value_id"> + <li class="form-group js_attribute_value" style="margin: 0;"> + <label class="col-form-label"> + <div> + <input type="radio" + t-attf-class="js_variant_change radio_input #{variant_id.attribute_id.create_variant}" + t-att-checked="first_possible_combination[variant_id_index] == value_id" + t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" + t-att-value="value_id.id" + t-att-data-value_id="value_id.product_attribute_value_id.id" + t-att-data-value_name="value_id.name" + t-att-data-attribute_name="value_id.attribute_id.name" + t-att-data-is_custom="value_id.is_custom" /> + <div class="radio_input_value"> + <span t-field="value_id.name"/> + <span class="badge badge-pill badge-secondary" t-if="value_id.price_extra"> + <!-- see note above about price_extra --> + <t t-esc="value_id.price_extra > 0 and '+' or '-'"/> + <span t-esc="abs(value_id.price_extra)" class="variant_price_extra" style="white-space: nowrap;" + t-options='{ + "widget": "monetary", + "from_currency": product.currency_id, + "display_currency": (pricelist or product).currency_id + }'/> + </span> + </div> + </div> + </label> + </li> + </t> + </ul> + </t> + + <t t-if="variant_id.attribute_id.type == 'color'"> + <ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-inline"> + <li t-foreach="variant_id.product_template_value_ids" t-as="value_id" class="list-inline-item"> + <label t-attf-style="background-color:#{value_id.html_color or value_id.name if not value_id.is_custom else ''}" + t-attf-class="css_attribute_color #{'active' if first_possible_combination[variant_id_index] == value_id else ''} #{'custom_value' if value_id.is_custom else ''}"> + <input type="radio" + t-attf-class="js_variant_change #{variant_id.attribute_id.create_variant}" + t-att-checked="first_possible_combination[variant_id_index] == value_id" + t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" + t-att-value="value_id.id" + t-att-title="value_id.name" + t-att-data-value_id="value_id.product_attribute_value_id.id" + t-att-data-value_name="value_id.name" + t-att-data-attribute_name="value_id.attribute_id.name" + t-att-data-is_custom="value_id.is_custom"/> + </label> + </li> + </ul> + </t> + </li> + </t> + </ul> + </template> +</odoo> diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py index 145cce89c1e..09f069f85ed 100644 --- a/addons/sale/wizard/__init__.py +++ b/addons/sale/wizard/__init__.py @@ -3,4 +3,3 @@ from . import payment_acquirer_onboarding_wizard from . import sale_make_invoice_advance -from . import sale_product_configurator
\ No newline at end of file diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py index 3bf801a604b..98f77d20956 100644 --- a/addons/sale_management/__manifest__.py +++ b/addons/sale_management/__manifest__.py @@ -47,7 +47,6 @@ The Dashboard for the Sales Manager will include 'views/sale_management_views.xml', 'views/digest_views.xml', 'views/sale_order_views.xml', - 'views/assets.xml', 'report/sale_report_templates.xml', ], 'demo': [ diff --git a/addons/sale_management/tests/test_sale_ui.py b/addons/sale_management/tests/test_sale_ui.py index 6ec2f4167c1..cf107b02ebc 100644 --- a/addons/sale_management/tests/test_sale_ui.py +++ b/addons/sale_management/tests/test_sale_ui.py @@ -7,119 +7,3 @@ class TestUi(odoo.tests.HttpCase): def test_01_sale_tour(self): self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_tour.ready", login="admin") - - def test_02_product_configurator(self): - # group_product_variant: use the product configurator - # group_sale_pricelist: display the pricelist to determine when it is changed after choosing - # the partner - self.env.ref('base.user_admin').write({ - 'groups_id': [ - (4, self.env.ref('product.group_product_variant').id), - (4, self.env.ref('product.group_sale_pricelist').id), - ], - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_tour.ready", login="admin") - - def test_03_product_configurator_advanced(self): - # group_product_variant: use the product configurator - # group_sale_pricelist: display the pricelist to determine when it is changed after choosing - # the partner - self.env.ref('base.user_admin').write({ - 'groups_id': [ - (4, self.env.ref('product.group_product_variant').id), - (4, self.env.ref('product.group_sale_pricelist').id), - ], - }) - - # Prepare relevant test data - # This is not included in demo data to avoid useless noise - product_attributes = self.env['product.attribute'].create([{ - 'name': 'PA1', - 'type': 'radio', - 'create_variant': 'dynamic' - }, { - 'name': 'PA2', - 'type': 'radio', - 'create_variant': 'always' - }, { - 'name': 'PA3', - 'type': 'radio', - 'create_variant': 'dynamic' - }, { - 'name': 'PA4', - 'type': 'select', - 'create_variant': 'no_variant' - }, { - 'name': 'PA5', - 'type': 'select', - 'create_variant': 'no_variant' - }, { - 'name': 'PA7', - 'type': 'color', - 'create_variant': 'no_variant' - }, { - 'name': 'PA8', - 'type': 'radio', - 'create_variant': 'no_variant' - }]) - - product_attribute_values = self.env['product.attribute.value'].create([{ - 'name': 'PAV' + str(i), - 'is_custom': i == 9, - 'attribute_id': product_attribute.id - } for i in range(1, 11) for product_attribute in product_attributes]) - - product_template_attribute_lines = self.env['product.template.attribute.line'].create([{ - 'attribute_id': product_attribute.id, - 'product_tmpl_id': self.env.ref("product.product_product_4").id, - 'value_ids': [(6, 0, product_attribute_values.filtered( - lambda product_attribute_value: product_attribute_value.attribute_id == product_attribute - ).ids)] - } for product_attribute in product_attributes]) - - self.env.ref("product.product_product_4").update({ - 'attribute_line_ids': [(4, product_template_attribute_line.id) for product_template_attribute_line in product_template_attribute_lines] - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_advanced_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_advanced_tour.ready", login="admin") - - def test_04_product_configurator_pricelist(self): - """The goal of this test is to make sure pricelist rules are correctly - applied on the backend product configurator. - Also testing B2C setting: no impact on the backend configurator. - """ - - admin = self.env.ref('base.user_admin') - - # Activate B2C - self.env.ref('account.group_show_line_subtotals_tax_included').users |= admin - self.env.ref('account.group_show_line_subtotals_tax_excluded').users -= admin - - # Active pricelist on SO - self.env.ref('product.group_sale_pricelist').users |= admin - - # Add a 15% tax on desk - tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 15}) - self.env.ref('product.product_product_4_product_template').taxes_id = tax - - # Remove tax from Conference Chair and Chair floor protection - self.env.ref('sale.product_product_1_product_template').taxes_id = None - self.env.ref('product.product_product_11_product_template').taxes_id = None - - # Make sure pricelist rule exist - product_template = self.env.ref('product.product_product_4_product_template') - pricelist = self.env.ref('product.list0') - - 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', - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_pricelist_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_pricelist_tour.ready", login="admin") diff --git a/addons/sale_management/views/assets.xml b/addons/sale_management/views/assets.xml deleted file mode 100644 index a5a3134949d..00000000000 --- a/addons/sale_management/views/assets.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <template id="assets_backend_inherit_sale_management" inherit_id="web.assets_backend" name="Sale management backend assets"> - <xpath expr="script[last()]" position="after"> - <script type="text/javascript" src="/sale_management/static/tests/product_configurator_test_ui.js"/> - <script type="text/javascript" src="/sale_management/static/tests/product_configurator_advanced_test_ui.js"/> - <script type="text/javascript" src="/sale_management/static/tests/product_configurator_pricelist_test_ui.js"/> - </xpath> - </template> -</odoo> diff --git a/addons/sale_product_configurator/__init__.py b/addons/sale_product_configurator/__init__.py new file mode 100644 index 00000000000..1f4530082b3 --- /dev/null +++ b/addons/sale_product_configurator/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers +from . import wizard diff --git a/addons/sale_product_configurator/__manifest__.py b/addons/sale_product_configurator/__manifest__.py new file mode 100644 index 00000000000..fd72a2fec6f --- /dev/null +++ b/addons/sale_product_configurator/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': "Sale Product Configurator", + 'version': '1.0', + 'category': 'Hidden', + 'summary': "Configure your products", + + 'description': """ + Technical module installed when the user checks the "module_sale_product_configurator" setting. + The main purpose is to override the sale_order view to allow configuring products in the SO form. + + It also enables the optional products feature. + """, + + 'depends': ['sale'], + 'data': [ + 'views/assets.xml', + 'views/templates.xml', + 'views/sale_views.xml', + 'wizard/sale_product_configurator_views.xml', + ], + 'demo': [ + 'data/sale_demo.xml', + ], +} diff --git a/addons/sale_product_configurator/controllers/__init__.py b/addons/sale_product_configurator/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/sale_product_configurator/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/sale/controllers/product_configurator.py b/addons/sale_product_configurator/controllers/main.py index 8ebcf879ba8..d991c4ff277 100644 --- a/addons/sale/controllers/product_configurator.py +++ b/addons/sale_product_configurator/controllers/main.py @@ -6,7 +6,7 @@ from odoo.http import request class ProductConfiguratorController(http.Controller): - @http.route(['/product_configurator/configure'], type='json', auth="user", methods=['POST']) + @http.route(['/sale_product_configurator/configure'], type='json', auth="user", methods=['POST']) def configure(self, product_id, pricelist_id, **kw): add_qty = int(kw.get('add_qty', 1)) product_template = request.env['product.template'].browse(int(product_id)) @@ -17,39 +17,24 @@ class ProductConfiguratorController(http.Controller): product_template = product_template.with_context(pricelist=pricelist.id, partner=request.env.user.partner_id) to_currency = pricelist.currency_id - return request.env['ir.ui.view'].render_template("sale.product_configurator_configure", { + return request.env['ir.ui.view'].render_template("sale_product_configurator.configure", { 'product': product_template, # to_currency deprecated, get it from the pricelist or product directly 'to_currency': to_currency, 'pricelist': pricelist, 'add_qty': add_qty, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions }) - @http.route(['/product_configurator/show_optional_products'], type='json', auth="user", methods=['POST']) + @http.route(['/sale_product_configurator/show_optional_products'], type='json', auth="user", methods=['POST']) def show_optional_products(self, product_id, variant_values, pricelist_id, **kw): pricelist = self._get_pricelist(pricelist_id) return self._show_optional_products(product_id, variant_values, pricelist, False, **kw) - @http.route(['/product_configurator/optional_product_items'], type='json', auth="user", methods=['POST']) + @http.route(['/sale_product_configurator/optional_product_items'], type='json', auth="user", methods=['POST']) def optional_product_items(self, product_id, pricelist_id, **kw): pricelist = self._get_pricelist(pricelist_id) return self._optional_product_items(product_id, pricelist, **kw) - @http.route(['/product_configurator/get_combination_info'], type='json', auth="user", methods=['POST']) - def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw): - combination = request.env['product.template.attribute.value'].browse(combination) - pricelist = self._get_pricelist(pricelist_id) - ProductTemplate = request.env['product.template'] - if 'context' in kw: - ProductTemplate = ProductTemplate.with_context(**kw.get('context')) - return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) - - @http.route(['/product_configurator/create_product_variant'], type='json', auth="user", methods=['POST']) - def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): - return request.env['product.template'].browse(int(product_template_id)).create_product_variant(product_template_attribute_value_ids) - def _optional_product_items(self, product_id, pricelist, **kw): add_qty = int(kw.get('add_qty', 1)) product = request.env['product.product'].with_context(self._get_product_context(pricelist, **kw)).browse(int(product_id)) @@ -64,7 +49,7 @@ class ProductConfiguratorController(http.Controller): # They are kept in the context since they are not linked to this product variant parent_combination |= product.env.context.get('no_variant_attribute_values') - return request.env['ir.ui.view'].render_template("sale.optional_product_items", { + return request.env['ir.ui.view'].render_template("sale_product_configurator.optional_product_items", { # product deprecated, it's not used in the view 'product': product, # reference_product deprecated, use parent_combination instead @@ -74,8 +59,6 @@ class ProductConfiguratorController(http.Controller): 'pricelist': pricelist, # to_currency deprecated, get from pricelist or product 'to_currency': to_currency, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions, 'add_qty': add_qty, }) @@ -101,7 +84,7 @@ class ProductConfiguratorController(http.Controller): if no_variant_attribute_values: product = product.with_context(no_variant_attribute_values=no_variant_attribute_values) - return request.env['ir.ui.view'].render_template("sale.optional_products_modal", { + return request.env['ir.ui.view'].render_template("sale_product_configurator.optional_products_modal", { 'product': product, 'combination': combination, 'add_qty': add_qty, @@ -115,21 +98,8 @@ class ProductConfiguratorController(http.Controller): # to_currency deprecated, get from pricelist or product 'to_currency': to_currency, 'handle_stock': handle_stock, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions, }) - def _get_attribute_exclusions(self, product, reference_product=None): - """deprecated, use product method""" - parent_combination = request.env['product.template.attribute.value'] - if reference_product: - parent_combination |= reference_product.product_template_attribute_value_ids - if reference_product.env.context.get('no_variant_attribute_values'): - # Add "no_variant" attribute values' exclusions - # They are kept in the context since they are not linked to this product variant - parent_combination |= reference_product.env.context.get('no_variant_attribute_values') - return product._get_attribute_exclusions(parent_combination, reference_product.name if reference_product else None) - def _get_product_context(self, pricelist=None, **kw): """deprecated, can be removed in master""" product_context = dict(request.context) @@ -140,10 +110,5 @@ class ProductConfiguratorController(http.Controller): return product_context - def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw): - """deprecated, use product method""" - combination = request.env['product.template.attribute.value'].browse(combination) - return request.env['product.template'].browse(product_template_id)._get_combination_info(combination, product_id, add_qty, pricelist) - def _get_pricelist(self, pricelist_id, pricelist_fallback=False): return request.env['product.pricelist'].browse(int(pricelist_id or 0)) diff --git a/addons/sale_product_configurator/data/sale_demo.xml b/addons/sale_product_configurator/data/sale_demo.xml new file mode 100644 index 00000000000..00683af8855 --- /dev/null +++ b/addons/sale_product_configurator/data/sale_demo.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <record id="product_product_1_product_template" model="product.template"> + <field name="name">Chair floor protection</field> + <field name="categ_id" ref="product.product_category_5"/> + <field name="list_price">12.0</field> + <field name="weight">0.01</field> + <field name="uom_id" ref="uom.product_uom_unit"/> + <field name="uom_po_id" ref="uom.product_uom_unit"/> + <field name="description_sale">Office chairs can harm your floor: protect it.</field> + <field name="image" type="base64" file="sale/static/img/floor_protection-image.png"/> + </record> + + <record id="product.product_product_4_product_template" model="product.template"> + <field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template')])]"/> + </record> + <record id="product.product_product_11_product_template" model="product.template"> + <field name="optional_product_ids" eval="[(6,0,[ref('product_product_1_product_template')])]"/> + </record> + <record id="product.product_product_13_product_template" model="product.template"> + <field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template')])]"/> + </record> + </data> +</odoo> diff --git a/addons/sale_product_configurator/models/product.py b/addons/sale_product_configurator/models/product.py new file mode 100644 index 00000000000..a47d6d4421a --- /dev/null +++ b/addons/sale_product_configurator/models/product.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + optional_product_ids = fields.Many2many( + 'product.template', 'product_optional_rel', 'src_id', 'dest_id', + string='Optional Products', help="Optional Products are suggested " + "whenever the customer hits *Add to Cart* (cross-sell strategy, " + "e.g. for computers: warranty, software, etc.).") diff --git a/addons/sale/static/src/js/product_configurator_controller.js b/addons/sale_product_configurator/static/src/js/product_configurator_controller.js index 7ca3d32c292..a7411a77370 100644 --- a/addons/sale/static/src/js/product_configurator_controller.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_controller.js @@ -1,10 +1,10 @@ -odoo.define('sale.ProductConfiguratorFormController', function (require) { +odoo.define('sale_product_configurator.ProductConfiguratorFormController', function (require) { "use strict"; var core = require('web.core'); var _t = core._t; var FormController = require('web.FormController'); -var OptionalProductsModal = require('sale.OptionalProductsModal'); +var OptionalProductsModal = require('sale_product_configurator.OptionalProductsModal'); var ProductConfiguratorFormController = FormController.extend({ custom_events: _.extend({}, FormController.prototype.custom_events, { @@ -127,8 +127,7 @@ var ProductConfiguratorFormController = FormController.extend({ pricelistId: self.renderer.pricelistId, okButtonText: _t('Confirm'), cancelButtonText: _t('Back'), - title: _t('Configure'), - context: self.initialState.context, + title: _t('Configure') }).open(); self.optionalProductsModal.on('options_empty', null, diff --git a/addons/sale/static/src/js/product_configurator_modal.js b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js index 3bcebf88725..9fc78b9528f 100644 --- a/addons/sale/static/src/js/product_configurator_modal.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js @@ -1,4 +1,4 @@ -odoo.define('sale.OptionalProductsModal', function (require) { +odoo.define('sale_product_configurator.OptionalProductsModal', function (require) { "use strict"; var ajax = require('web.ajax'); @@ -79,7 +79,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi willStart: function () { var self = this; - var uri = this._getUri("/product_configurator/show_optional_products"); + var uri = this._getUri("/sale_product_configurator/show_optional_products"); var getModalContent = ajax.jsonRpc(uri, 'call', { product_id: self.rootProduct.product_id, variant_values: self.rootProduct.variant_values, @@ -361,7 +361,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi ).then(function (productId) { $parent.find('.product_id').val(productId); - ajax.jsonRpc(self._getUri("/product_configurator/optional_product_items"), 'call', { + ajax.jsonRpc(self._getUri("/sale_product_configurator/optional_product_items"), 'call', { 'product_id': productId, 'pricelist_id': self.pricelistId || false, }).then(function (addedItem) { diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js index f3f437461ea..62a7d7ea06b 100644 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js @@ -1,4 +1,4 @@ -odoo.define('sale.ProductConfiguratorFormRenderer', function (require) { +odoo.define('sale_product_configurator.ProductConfiguratorFormRenderer', function (require) { "use strict"; var FormRenderer = require('web.FormRenderer'); diff --git a/addons/sale/static/src/js/product_configurator_view.js b/addons/sale_product_configurator/static/src/js/product_configurator_view.js index 5a3bb4401be..d3294deb42d 100644 --- a/addons/sale/static/src/js/product_configurator_view.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_view.js @@ -1,8 +1,8 @@ -odoo.define('sale.ProductConfiguratorFormView', function (require) { +odoo.define('sale_product_configurator.ProductConfiguratorFormView', function (require) { "use strict"; -var ProductConfiguratorFormController = require('sale.ProductConfiguratorFormController'); -var ProductConfiguratorFormRenderer = require('sale.ProductConfiguratorFormRenderer'); +var ProductConfiguratorFormController = require('sale_product_configurator.ProductConfiguratorFormController'); +var ProductConfiguratorFormRenderer = require('sale_product_configurator.ProductConfiguratorFormRenderer'); var FormView = require('web.FormView'); var viewRegistry = require('web.view_registry'); diff --git a/addons/sale/static/tests/product_configurator.test.js b/addons/sale_product_configurator/static/tests/product_configurator.test.js index c751eb6c111..e39fa64644d 100644 --- a/addons/sale/static/tests/product_configurator.test.js +++ b/addons/sale_product_configurator/static/tests/product_configurator.test.js @@ -2,7 +2,7 @@ odoo.define('sale.product.configurator.tests', function (require) { "use strict"; var FormView = require('web.FormView'); - var ProductConfiguratorFormView = require('sale.ProductConfiguratorFormView'); + var ProductConfiguratorFormView = require('sale_product_configurator.ProductConfiguratorFormView'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; diff --git a/addons/sale_management/static/tests/product_configurator_advanced_test_ui.js b/addons/sale_product_configurator/static/tests/product_configurator_advanced_test_ui.js index 074da95f06e..074da95f06e 100644 --- a/addons/sale_management/static/tests/product_configurator_advanced_test_ui.js +++ b/addons/sale_product_configurator/static/tests/product_configurator_advanced_test_ui.js diff --git a/addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js b/addons/sale_product_configurator/static/tests/product_configurator_pricelist_test_ui.js index 0cb9b46e530..0cb9b46e530 100644 --- a/addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js +++ b/addons/sale_product_configurator/static/tests/product_configurator_pricelist_test_ui.js diff --git a/addons/sale_management/static/tests/product_configurator_test_ui.js b/addons/sale_product_configurator/static/tests/product_configurator_test_ui.js index 03f5a0a8bbc..03f5a0a8bbc 100644 --- a/addons/sale_management/static/tests/product_configurator_test_ui.js +++ b/addons/sale_product_configurator/static/tests/product_configurator_test_ui.js diff --git a/addons/sale_product_configurator/tests/__init__.py b/addons/sale_product_configurator/tests/__init__.py new file mode 100644 index 00000000000..c1cb786c919 --- /dev/null +++ b/addons/sale_product_configurator/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_sale_product_configurator_ui diff --git a/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py new file mode 100644 index 00000000000..a37e5da7f8a --- /dev/null +++ b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo.tests + + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(odoo.tests.HttpCase): + def test_01_product_configurator(self): + # To be able to test the product configurator, admin user must have access to "variants and options" feature, so we give him the right group for that + self.env.ref('base.user_admin').write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]}) + + self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_tour.ready", login="admin") + + def test_02_product_configurator_advanced(self): + # group_product_variant: use the product configurator + # group_sale_pricelist: display the pricelist to determine when it is changed after choosing + # the partner + self.env.ref('base.user_admin').write({ + 'groups_id': [ + (4, self.env.ref('product.group_product_variant').id), + (4, self.env.ref('product.group_sale_pricelist').id), + ], + }) + + # Prepare relevant test data + # This is not included in demo data to avoid useless noise + product_attributes = self.env['product.attribute'].create([{ + 'name': 'PA1', + 'type': 'radio', + 'create_variant': 'dynamic' + }, { + 'name': 'PA2', + 'type': 'radio', + 'create_variant': 'always' + }, { + 'name': 'PA3', + 'type': 'radio', + 'create_variant': 'dynamic' + }, { + 'name': 'PA4', + 'type': 'select', + 'create_variant': 'no_variant' + }, { + 'name': 'PA5', + 'type': 'select', + 'create_variant': 'no_variant' + }, { + 'name': 'PA7', + 'type': 'color', + 'create_variant': 'no_variant' + }, { + 'name': 'PA8', + 'type': 'radio', + 'create_variant': 'no_variant' + }]) + + product_attribute_values = self.env['product.attribute.value'].create([{ + 'name': 'PAV' + str(i), + 'is_custom': i == 9, + 'attribute_id': product_attribute.id + } for i in range(1, 11) for product_attribute in product_attributes]) + + product_template_attribute_lines = self.env['product.template.attribute.line'].create([{ + 'attribute_id': product_attribute.id, + 'product_tmpl_id': self.env.ref("product.product_product_4").id, + 'value_ids': [(6, 0, product_attribute_values.filtered( + lambda product_attribute_value: product_attribute_value.attribute_id == product_attribute + ).ids)] + } for product_attribute in product_attributes]) + + self.env.ref("product.product_product_4").update({ + 'attribute_line_ids': [(4, product_template_attribute_line.id) for product_template_attribute_line in product_template_attribute_lines] + }) + + self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_advanced_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_advanced_tour.ready", login="admin") + + def test_03_product_configurator_edition(self): + # To be able to test the product configurator, admin user must have access to "variants and options" feature, so we give him the right group for that + self.env.ref('base.user_admin').write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]}) + + self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_edition_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_edition_tour.ready", login="admin") + + def test_04_product_configurator_single_custom_value(self): + # group_product_variant: use the product configurator + # group_sale_pricelist: display the pricelist to determine when it is changed after choosing + # the partner + self.env.ref('base.user_admin').write({ + 'groups_id': [ + (4, self.env.ref('product.group_product_variant').id), + (4, self.env.ref('product.group_sale_pricelist').id), + ], + }) + + # Prepare relevant test data + # This is not included in demo data to avoid useless noise + product_attributes = self.env['product.attribute'].create([{ + 'name': 'product attribute', + 'type': 'radio', + 'create_variant': 'always' + }]) + + product_attribute_values = self.env['product.attribute.value'].create([{ + 'name': 'single product attribute value', + 'is_custom': True, + 'attribute_id': product_attributes[0].id + }]) + + product_template_attribute_lines = self.env['product.template.attribute.line'].create([{ + 'attribute_id': product_attributes[0].id, + 'product_tmpl_id': self.env.ref("product.product_product_4").id, + 'value_ids': [(6, 0, [product_attribute_values[0].id])] + }]) + + self.env.ref("product.product_product_4").update({ + 'attribute_line_ids': [(4, product_template_attribute_lines[0].id)] + }) + + self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_single_custom_attribute_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_single_custom_attribute_tour.ready", login="admin") + + def test_05_product_configurator_pricelist(self): + """The goal of this test is to make sure pricelist rules are correctly + applied on the backend product configurator. + Also testing B2C setting: no impact on the backend configurator. + """ + + admin = self.env.ref('base.user_admin') + + # Activate B2C + self.env.ref('account.group_show_line_subtotals_tax_included').users |= admin + self.env.ref('account.group_show_line_subtotals_tax_excluded').users -= admin + + # Active pricelist on SO + self.env.ref('product.group_sale_pricelist').users |= admin + + # Add a 15% tax on desk + tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 15}) + self.env.ref('product.product_product_4_product_template').taxes_id = tax + + # Remove tax from Conference Chair and Chair floor protection + self.env.ref('sale_product_configurator.product_product_1_product_template').taxes_id = None + self.env.ref('product.product_product_11_product_template').taxes_id = None + + # Make sure pricelist rule exist + product_template = self.env.ref('product.product_product_4_product_template') + pricelist = self.env.ref('product.list0') + + 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', + }) + + self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_pricelist_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_pricelist_tour.ready", login="admin")
\ No newline at end of file diff --git a/addons/sale_product_configurator/views/assets.xml b/addons/sale_product_configurator/views/assets.xml new file mode 100644 index 00000000000..3e68f30d810 --- /dev/null +++ b/addons/sale_product_configurator/views/assets.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" inherit_id="web.assets_backend" name="sale_product_configurator assets backend"> + <xpath expr="link[last()]" position="after"> + <link rel="stylesheet" type="text/scss" href="/sale_product_configurator/static/src/scss/product_configurator_widget.scss"/> + </xpath> + <xpath expr="script[last()]" position="after"> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/tour.js"></script> + <script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_renderer.js"></script> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_controller.js"></script> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_view.js"></script> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_widget.js"/> + <script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_modal.js"></script> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator_test_ui.js"/> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator_advanced_test_ui.js"/> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator_edition_test_ui.js"/> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator_single_custom_attribute_test_ui.js"/> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator_pricelist_test_ui.js"/> + </xpath> + </template> + <template id="qunit_suite" inherit_id="web.qunit_suite" name="sale_product_configurator_tests"> + <xpath expr="//t[@t-set='head']" position="inside"> + <script type="text/javascript" src="/sale_product_configurator/static/tests/product_configurator.test.js"></script> + </xpath> + </template> +</odoo> diff --git a/addons/sale_product_configurator/views/sale_views.xml b/addons/sale_product_configurator/views/sale_views.xml new file mode 100644 index 00000000000..499cb2c7eb0 --- /dev/null +++ b/addons/sale_product_configurator/views/sale_views.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="product_template_view_form" model="ir.ui.view"> + <field name="name">product.template.form.inherit.sale.product.configurator</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_form_view"/> + <field name="arch" type="xml"> + <xpath expr="//group[@name='sale']" position="after"> + <group name="options" groups="product.group_product_variant"> + <group string="Options"> + <field name="optional_product_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('id', '!=', active_id)]" /> + </group> + </group> + </xpath> + </field> + </record> +</odoo> diff --git a/addons/sale/views/sale_product_configurator_templates.xml b/addons/sale_product_configurator/views/templates.xml index 1f0ec29d534..534a199dc70 100644 --- a/addons/sale/views/sale_product_configurator_templates.xml +++ b/addons/sale_product_configurator/views/templates.xml @@ -2,12 +2,12 @@ <odoo> <template id="optional_products_modal" name="Optional Products"> <main class="modal-body"> - <t t-call="sale.product_configurator_configure_optional_products" /> + <t t-call="sale_product_configurator.configure_optional_products" /> </main> </template> <!-- backend --> - <template id="product_configurator_configure" name="Configure"> + <template id="configure" name="Configure"> <div class="js_product main_product"> <t t-set="first_possible_combination" t-value="product._get_first_possible_combination()"/> @@ -64,7 +64,7 @@ </template> <!-- modal: full table, currenclty selected products at top --> - <template id="product_configurator_configure_optional_products"> + <template id="configure_optional_products"> <table class="table table-striped table-sm"> <thead> <tr> @@ -141,7 +141,7 @@ </td> </tr> <tr t-if="product.optional_product_ids" class="o_select_options"><td colspan="4"><h4>Available Options:</h4></td></tr> - <t t-call="sale.optional_product_items"> + <t t-call="sale_product_configurator.optional_product_items"> <t t-set="parent_combination" t-value="combination"/> </t> </tbody> @@ -201,112 +201,4 @@ </t> </t> </template> - - - - <template id="variants"> - <t t-set="attribute_exclusions" t-value="product._get_attribute_exclusions(parent_combination, parent_name)"/> - <ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)"> - <t t-foreach="product._get_valid_product_template_attribute_lines()" t-as="variant_id"> - <!-- Attributes selection is hidden if there is only one value available and it's not a custom value --> - <li t-att-data-attribute_id="variant_id.attribute_id.id" - t-att-data-attribute_name="variant_id.attribute_id.name" - t-attf-class="variant_attribute #{'d-none' if len(variant_id.product_template_value_ids) == 1 and not variant_id.product_template_value_ids[0].is_custom else ''}"> - - <strong t-field="variant_id.attribute_id.name" class="attribute_name"/> - - <t t-if="variant_id.attribute_id.type == 'select'"> - <select - t-att-data-attribute_id="variant_id.attribute_id.id" - t-attf-class="form-control js_variant_change #{variant_id.attribute_id.create_variant}" - t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"> - <t t-foreach="variant_id.product_template_value_ids" t-as="value_id"> - <option t-att-value="value_id.id" - t-att-data-value_id="value_id.product_attribute_value_id.id" - t-att-data-value_name="value_id.name" - t-att-data-attribute_name="value_id.attribute_id.name" - t-att-data-is_custom="value_id.is_custom" - t-att-selected="first_possible_combination[variant_id_index] == value_id"> - <span t-field="value_id.name"/> - <span t-if="value_id.price_extra" class="badge badge-pill badge-secondary"> - <!-- - price_extra is displayed as catalog price instead of - price after pricelist because it is impossible to - compute. Indeed, the pricelist rule might depend on the - selected variant, so the price_extra will be different - depending on the selected combination. The price of an - attribute is therefore variable and it's not very - accurate to display it. - --> - <t t-esc="value_id.price_extra > 0 and '+' or '-'"/> - <span t-esc="abs(value_id.price_extra)" class="variant_price_extra" style="white-space: nowrap;" - t-options='{ - "widget": "monetary", - "from_currency": product.currency_id, - "display_currency": (pricelist or product).currency_id - }'/> - </span> - </option> - </t> - </select> - </t> - - <t t-if="variant_id.attribute_id.type == 'radio'"> - <ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-unstyled"> - <t t-foreach="variant_id.product_template_value_ids" t-as="value_id"> - <li class="form-group js_attribute_value" style="margin: 0;"> - <label class="col-form-label"> - <div> - <input type="radio" - t-attf-class="js_variant_change radio_input #{variant_id.attribute_id.create_variant}" - t-att-checked="first_possible_combination[variant_id_index] == value_id" - t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" - t-att-value="value_id.id" - t-att-data-value_id="value_id.product_attribute_value_id.id" - t-att-data-value_name="value_id.name" - t-att-data-attribute_name="value_id.attribute_id.name" - t-att-data-is_custom="value_id.is_custom" /> - <div class="radio_input_value"> - <span t-field="value_id.name"/> - <span class="badge badge-pill badge-secondary" t-if="value_id.price_extra"> - <!-- see note above about price_extra --> - <t t-esc="value_id.price_extra > 0 and '+' or '-'"/> - <span t-esc="abs(value_id.price_extra)" class="variant_price_extra" style="white-space: nowrap;" - t-options='{ - "widget": "monetary", - "from_currency": product.currency_id, - "display_currency": (pricelist or product).currency_id - }'/> - </span> - </div> - </div> - </label> - </li> - </t> - </ul> - </t> - - <t t-if="variant_id.attribute_id.type == 'color'"> - <ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-inline"> - <li t-foreach="variant_id.product_template_value_ids" t-as="value_id" class="list-inline-item"> - <label t-attf-style="background-color:#{value_id.html_color or value_id.name if not value_id.is_custom else ''}" - t-attf-class="css_attribute_color #{'active' if first_possible_combination[variant_id_index] == value_id else ''} #{'custom_value' if value_id.is_custom else ''}"> - <input type="radio" - t-attf-class="js_variant_change #{variant_id.attribute_id.create_variant}" - t-att-checked="first_possible_combination[variant_id_index] == value_id" - t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" - t-att-value="value_id.id" - t-att-title="value_id.name" - t-att-data-value_id="value_id.product_attribute_value_id.id" - t-att-data-value_name="value_id.name" - t-att-data-attribute_name="value_id.attribute_id.name" - t-att-data-is_custom="value_id.is_custom"/> - </label> - </li> - </ul> - </t> - </li> - </t> - </ul> - </template> </odoo> diff --git a/addons/sale/wizard/sale_product_configurator.py b/addons/sale_product_configurator/wizard/sale_product_configurator.py index 75563872928..75563872928 100644 --- a/addons/sale/wizard/sale_product_configurator.py +++ b/addons/sale_product_configurator/wizard/sale_product_configurator.py diff --git a/addons/sale/wizard/sale_product_configurator_views.xml b/addons/sale_product_configurator/wizard/sale_product_configurator_views.xml index 9b29c3f9183..a3d3eddfee9 100644 --- a/addons/sale/wizard/sale_product_configurator_views.xml +++ b/addons/sale_product_configurator/wizard/sale_product_configurator_views.xml @@ -15,4 +15,4 @@ </form> </field> </record> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 6a2daeee7da..9890566f03b 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -25,6 +25,17 @@ </div> </div> </div> + <div class="col-12 col-lg-6 o_setting_box"> + <div class="o_setting_left_pane"> + <field name="module_sale_product_configurator"/> + </div> + <div class="o_setting_right_pane"> + <label for="module_sale_product_configurator"/> + <div class="text-muted"> + Configure your attributes with exclusions and select optional products + </div> + </div> + </div> <div class="col-12 col-lg-6 o_setting_box" id="digital_content_setting" title="Provide customers with product-specific links or downloadable content in the confirmation page of the checkout process if the payment gets through. To do so, attach some files to a product using the new Files button and publish them."> <div class="o_setting_left_pane"> <field name="module_website_sale_digital"/> |
