diff --git a/website_sale_b2x_alt_price/README.rst b/website_sale_b2x_alt_price/README.rst new file mode 100644 index 0000000000..41c1a48d22 --- /dev/null +++ b/website_sale_b2x_alt_price/README.rst @@ -0,0 +1,147 @@ +================================================= +Alternative (un)taxed prices display on eCommerce +================================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8375402946906b5643a62ab7bfb8679e5140ba6d0df5dfd6f58cb22b19871e81 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github + :target: https://github.com/OCA/e-commerce/tree/17.0/website_sale_b2x_alt_price + :alt: OCA/e-commerce +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/e-commerce-17-0/e-commerce-17-0-website_sale_b2x_alt_price + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module takes into account if your eCommerce is in B2B or B2C mode, +and displays the alternative pricing next to the main one. + +For example, if your website is B2B (main price is without taxes), you +would see it similar to this: + +|b2b-features| + +If your website is B2C (main price is with taxes), then it looks like +this: + +|b2c-features| + +It also displays the alternative price, including discounts, updated +automatically when the user is choosing alternatives in the product +detail page: + +|details-features| + +.. |b2b-features| image:: https://raw.githubusercontent.com/OCA/e-commerce/17.0/website_sale_b2x_alt_price/static/description/b2b-features.png +.. |b2c-features| image:: https://raw.githubusercontent.com/OCA/e-commerce/17.0/website_sale_b2x_alt_price/static/description/b2c-features.png +.. |details-features| image:: https://raw.githubusercontent.com/OCA/e-commerce/17.0/website_sale_b2x_alt_price/static/description/details-features.gif + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To choose if you want to display or not the alternative price in the +website products list, just enable or disable the feature from the +"Customize" tab of the web editor. This can be done independently per +website: + +|toggle-list| + +You can also toggle the behavior similarly in the product details page. +This can also be done independently per website: + +|toggle-detail| + +.. |toggle-list| image:: https://raw.githubusercontent.com/OCA/e-commerce/17.0/website_sale_b2x_alt_price/static/description/toggle-list.gif +.. |toggle-detail| image:: https://raw.githubusercontent.com/OCA/e-commerce/17.0/website_sale_b2x_alt_price/static/description/toggle-detail.gif + +Usage +===== + +To use this module, you need to: + +1. Create a product. +2. Set some customer taxes on it. +3. Publish it. +4. Go see it on your website. It will display the alternative price. + +Known issues / Roadmap +====================== + +- Computing this requires 1 or 2 extra calls to ``taxes.compute_all()`` + per product. This could be expensive in terms of performance, but the + truth is that it is also not very optimized upstream in Odoo. Maybe + optimize this part in Odoo and in this module? Or maybe apply some + hack to avoid recomputing this? Maybe cache calls to that method? Is + it really a problem in the Real World®? + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Jairo Llopis + - Carlos Roca + - Carlos López + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Yajo| image:: https://github.com/Yajo.png?size=40px + :target: https://github.com/Yajo + :alt: Yajo + +Current `maintainer `__: + +|maintainer-Yajo| + +This module is part of the `OCA/e-commerce `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_sale_b2x_alt_price/__init__.py b/website_sale_b2x_alt_price/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/website_sale_b2x_alt_price/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/website_sale_b2x_alt_price/__manifest__.py b/website_sale_b2x_alt_price/__manifest__.py new file mode 100644 index 0000000000..692986ba97 --- /dev/null +++ b/website_sale_b2x_alt_price/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2020 Jairo Llopis - Tecnativa +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +{ + "name": "Alternative (un)taxed prices display on eCommerce", + "summary": "Display prices with(out) taxes in eCommerce, complementing normal mode", + "version": "17.0.1.0.0", + "development_status": "Production/Stable", + "category": "Website", + "website": "https://github.com/OCA/e-commerce", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["Yajo"], + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": ["website_sale"], + "data": ["templates/product.xml"], + "assets": { + "web.assets_frontend": [ + "/website_sale_b2x_alt_price/static/src/js/product_configurator_mixin.esm.js", + ], + "web.assets_tests": [ + "/website_sale_b2x_alt_price/static/tests/tours/b2b.esm.js", + "/website_sale_b2x_alt_price/static/tests/tours/b2c.esm.js", + ], + }, +} diff --git a/website_sale_b2x_alt_price/i18n/ca.po b/website_sale_b2x_alt_price/i18n/ca.po new file mode 100644 index 0000000000..657546c4bf --- /dev/null +++ b/website_sale_b2x_alt_price/i18n/ca.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_b2x_alt_price +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-25 11:25+0000\n" +"PO-Revision-Date: 2021-02-25 12:26+0100\n" +"Last-Translator: Carlos \n" +"Language-Team: none\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Poedit 2.0.6\n" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (with taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (without taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model:ir.model,name:website_sale_b2x_alt_price.model_product_template +msgid "Product Template" +msgstr "Plantilla de producte" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "with taxes" +msgstr "amb taxes" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "without taxes" +msgstr "sense taxes" diff --git a/website_sale_b2x_alt_price/i18n/es.po b/website_sale_b2x_alt_price/i18n/es.po new file mode 100644 index 0000000000..05ef1a886a --- /dev/null +++ b/website_sale_b2x_alt_price/i18n/es.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_b2x_alt_price +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-25 11:25+0000\n" +"PO-Revision-Date: 2023-07-05 11:10+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (with taxes)" +msgstr " (con impuestos)" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (without taxes)" +msgstr " (sin impuestos)" + +#. module: website_sale_b2x_alt_price +#: model:ir.model,name:website_sale_b2x_alt_price.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "with taxes" +msgstr "con impuestos" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "without taxes" +msgstr "sin impuestos" diff --git a/website_sale_b2x_alt_price/i18n/fr.po b/website_sale_b2x_alt_price/i18n/fr.po new file mode 100644 index 0000000000..c93360f318 --- /dev/null +++ b/website_sale_b2x_alt_price/i18n/fr.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_b2x_alt_price +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-25 11:25+0000\n" +"PO-Revision-Date: 2021-02-25 12:26+0100\n" +"Last-Translator: Carlos \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Poedit 2.0.6\n" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (with taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (without taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model:ir.model,name:website_sale_b2x_alt_price.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "with taxes" +msgstr "avec taxes" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "without taxes" +msgstr "sans taxes" diff --git a/website_sale_b2x_alt_price/i18n/website_sale_b2x_alt_price.pot b/website_sale_b2x_alt_price/i18n/website_sale_b2x_alt_price.pot new file mode 100644 index 0000000000..9bfb21fd65 --- /dev/null +++ b/website_sale_b2x_alt_price/i18n/website_sale_b2x_alt_price.pot @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_b2x_alt_price +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (with taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.sale_order_portal_template +msgid " (without taxes)" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model:ir.model,name:website_sale_b2x_alt_price.model_product_template +msgid "Product Template" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "with taxes" +msgstr "" + +#. module: website_sale_b2x_alt_price +#: model_terms:ir.ui.view,arch_db:website_sale_b2x_alt_price.alt_price +msgid "without taxes" +msgstr "" diff --git a/website_sale_b2x_alt_price/models/__init__.py b/website_sale_b2x_alt_price/models/__init__.py new file mode 100644 index 0000000000..e8fa8f6bf1 --- /dev/null +++ b/website_sale_b2x_alt_price/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/website_sale_b2x_alt_price/models/product_template.py b/website_sale_b2x_alt_price/models/product_template.py new file mode 100644 index 0000000000..d15204b711 --- /dev/null +++ b/website_sale_b2x_alt_price/models/product_template.py @@ -0,0 +1,98 @@ +# Copyright 2020 Jairo Llopis - Tecnativa +# Copyright 2024 Carlos Lopez - Tecnativa +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _get_combination_info( + self, + combination=False, + product_id=False, + add_qty=1, + parent_combination=False, + only_template=False, + ): + """Include alternative (un)taxed amount.""" + combination_info = super()._get_combination_info( + combination=combination, + product_id=product_id, + add_qty=add_qty, + parent_combination=parent_combination, + only_template=only_template, + ) + product = ( + self.env["product.product"].browse(combination_info["product_id"]) or self + ) + combination_info.update( + self._get_alt_prices( + product, + combination_info["list_price"], + combination_info["price"], + combination_info["has_discounted_price"], + ) + ) + return combination_info + + def _get_sales_prices(self, pricelist, fiscal_position): + res = super()._get_sales_prices(pricelist, fiscal_position) + website = self.env["website"].get_current_website(fallback=False) + currency = website.currency_id + + for template in self: + price_info = res[template.id] + list_price = price_info.get("base_price") or price_info["price_reduce"] + price = price_info["price_reduce"] + has_discounted_price = False + if pricelist.discount_policy == "without_discount": + has_discounted_price = currency.compare_amounts(list_price, price) == 1 + alt_price_info = self._get_alt_prices( + template, list_price, price, has_discounted_price + ) + price_info.update(alt_price_info, has_discounted_price=has_discounted_price) + return res + + def _get_alt_prices(self, product, list_price, price, has_discounted_price): + website = self.env["website"].get_current_website(fallback=False) + if not website: + return {} + partner = self.env.user.partner_id + company_id = website.company_id + pricelist = website.pricelist_id + # The list_price is always the price of one. + quantity_1 = 1 + # Obtain the inverse field of the normal b2b/b2c behavior + alt_field = ( + "total_included" + if website.show_line_subtotals_tax_selection == "tax_excluded" + else "total_excluded" + ) + # Obtain taxes that apply to the product and context + taxes = partner.property_account_position_id.map_tax( + product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id) + ).with_context(force_price_include=alt_field == "total_excluded") + # Obtain alt prices + # TODO Cache upstream calls to compute_all() and get results from there + alt_list_price = alt_price = taxes.compute_all( + price, + pricelist.currency_id, + quantity_1, + product, + partner, + )[alt_field] + if has_discounted_price: + alt_list_price = taxes.compute_all( + list_price, + pricelist.currency_id, + quantity_1, + product, + partner, + )[alt_field] + return { + "alt_price": alt_price, + "alt_list_price": alt_list_price, + "alt_field": alt_field, + } diff --git a/website_sale_b2x_alt_price/pyproject.toml b/website_sale_b2x_alt_price/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_sale_b2x_alt_price/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_sale_b2x_alt_price/readme/CONFIGURE.md b/website_sale_b2x_alt_price/readme/CONFIGURE.md new file mode 100644 index 0000000000..11797ee35f --- /dev/null +++ b/website_sale_b2x_alt_price/readme/CONFIGURE.md @@ -0,0 +1,10 @@ +To choose if you want to display or not the alternative price in the +website products list, just enable or disable the feature from the +"Customize" tab of the web editor. This can be done independently per website: + +![toggle-list](../static/description/toggle-list.gif) + +You can also toggle the behavior similarly in the product details page. +This can also be done independently per website: + +![toggle-detail](../static/description/toggle-detail.gif) diff --git a/website_sale_b2x_alt_price/readme/CONTRIBUTORS.md b/website_sale_b2x_alt_price/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..7aca7348d8 --- /dev/null +++ b/website_sale_b2x_alt_price/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Tecnativa](https://www.tecnativa.com/): + - Jairo Llopis + - Carlos Roca + - Carlos López diff --git a/website_sale_b2x_alt_price/readme/DESCRIPTION.md b/website_sale_b2x_alt_price/readme/DESCRIPTION.md new file mode 100644 index 0000000000..2b1db31691 --- /dev/null +++ b/website_sale_b2x_alt_price/readme/DESCRIPTION.md @@ -0,0 +1,18 @@ +This module takes into account if your eCommerce is in B2B or B2C mode, +and displays the alternative pricing next to the main one. + +For example, if your website is B2B (main price is without taxes), you +would see it similar to this: + +![b2b-features](../static/description/b2b-features.png) + +If your website is B2C (main price is with taxes), then it looks like +this: + +![b2c-features](../static/description/b2c-features.png) + +It also displays the alternative price, including discounts, updated +automatically when the user is choosing alternatives in the product +detail page: + +![details-features](../static/description/details-features.gif) diff --git a/website_sale_b2x_alt_price/readme/ROADMAP.md b/website_sale_b2x_alt_price/readme/ROADMAP.md new file mode 100644 index 0000000000..29555164f0 --- /dev/null +++ b/website_sale_b2x_alt_price/readme/ROADMAP.md @@ -0,0 +1,6 @@ +- Computing this requires 1 or 2 extra calls to `taxes.compute_all()` + per product. This could be expensive in terms of performance, but the + truth is that it is also not very optimized upstream in Odoo. Maybe + optimize this part in Odoo and in this module? Or maybe apply some + hack to avoid recomputing this? Maybe cache calls to that method? Is + it really a problem in the Real World®? diff --git a/website_sale_b2x_alt_price/readme/USAGE.md b/website_sale_b2x_alt_price/readme/USAGE.md new file mode 100644 index 0000000000..82e3a52148 --- /dev/null +++ b/website_sale_b2x_alt_price/readme/USAGE.md @@ -0,0 +1,6 @@ +To use this module, you need to: + +1. Create a product. +2. Set some customer taxes on it. +3. Publish it. +4. Go see it on your website. It will display the alternative price. diff --git a/website_sale_b2x_alt_price/static/description/b2b-features.png b/website_sale_b2x_alt_price/static/description/b2b-features.png new file mode 100644 index 0000000000..025ea9ba33 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/b2b-features.png differ diff --git a/website_sale_b2x_alt_price/static/description/b2c-features.png b/website_sale_b2x_alt_price/static/description/b2c-features.png new file mode 100644 index 0000000000..5a509b1a36 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/b2c-features.png differ diff --git a/website_sale_b2x_alt_price/static/description/details-features.gif b/website_sale_b2x_alt_price/static/description/details-features.gif new file mode 100644 index 0000000000..388953fe32 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/details-features.gif differ diff --git a/website_sale_b2x_alt_price/static/description/icon.png b/website_sale_b2x_alt_price/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/icon.png differ diff --git a/website_sale_b2x_alt_price/static/description/index.html b/website_sale_b2x_alt_price/static/description/index.html new file mode 100644 index 0000000000..f3a1e5791b --- /dev/null +++ b/website_sale_b2x_alt_price/static/description/index.html @@ -0,0 +1,476 @@ + + + + + +Alternative (un)taxed prices display on eCommerce + + + +
+

Alternative (un)taxed prices display on eCommerce

+ + +

Production/Stable License: LGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runboat

+

This module takes into account if your eCommerce is in B2B or B2C mode, +and displays the alternative pricing next to the main one.

+

For example, if your website is B2B (main price is without taxes), you +would see it similar to this:

+

b2b-features

+

If your website is B2C (main price is with taxes), then it looks like +this:

+

b2c-features

+

It also displays the alternative price, including discounts, updated +automatically when the user is choosing alternatives in the product +detail page:

+

details-features

+

Table of contents

+ +
+

Configuration

+

To choose if you want to display or not the alternative price in the +website products list, just enable or disable the feature from the +“Customize” tab of the web editor. This can be done independently per +website:

+

toggle-list

+

You can also toggle the behavior similarly in the product details page. +This can also be done independently per website:

+

toggle-detail

+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Create a product.
  2. +
  3. Set some customer taxes on it.
  4. +
  5. Publish it.
  6. +
  7. Go see it on your website. It will display the alternative price.
  8. +
+
+
+

Known issues / Roadmap

+
    +
  • Computing this requires 1 or 2 extra calls to taxes.compute_all() +per product. This could be expensive in terms of performance, but the +truth is that it is also not very optimized upstream in Odoo. Maybe +optimize this part in Odoo and in this module? Or maybe apply some +hack to avoid recomputing this? Maybe cache calls to that method? Is +it really a problem in the Real World®?
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Jairo Llopis
    • +
    • Carlos Roca
    • +
    • Carlos López
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

Yajo

+

This module is part of the OCA/e-commerce project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/website_sale_b2x_alt_price/static/description/toggle-detail.gif b/website_sale_b2x_alt_price/static/description/toggle-detail.gif new file mode 100644 index 0000000000..c6f27e64d5 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/toggle-detail.gif differ diff --git a/website_sale_b2x_alt_price/static/description/toggle-list.gif b/website_sale_b2x_alt_price/static/description/toggle-list.gif new file mode 100644 index 0000000000..5946a233d1 Binary files /dev/null and b/website_sale_b2x_alt_price/static/description/toggle-list.gif differ diff --git a/website_sale_b2x_alt_price/static/src/js/product_configurator_mixin.esm.js b/website_sale_b2x_alt_price/static/src/js/product_configurator_mixin.esm.js new file mode 100644 index 0000000000..96883a7bbc --- /dev/null +++ b/website_sale_b2x_alt_price/static/src/js/product_configurator_mixin.esm.js @@ -0,0 +1,27 @@ +/** @odoo-module **/ +/* Copyright 2020 Jairo Llopis - Tecnativa + * Copyright 2022 Carlos Roca - Tecnativa + * Copyright 2024 Carlos Lopez - Tecnativa + * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +import {WebsiteSale} from "@website_sale/js/website_sale"; + +WebsiteSale.include({ + /** + * Add alt price onchange to the regular _onChangeCombination method. + * + * @override + */ + _onChangeCombination: function (ev, $parent, combination) { + // Write new alt prices + $parent + .find(".js_alt_price .oe_currency_value") + .text(this._priceToStr(combination.alt_price)); + $parent + .find(".js_alt_list_price") + .toggleClass("d-none", !combination.has_discounted_price) + .find(".oe_currency_value") + .text(this._priceToStr(combination.alt_list_price)); + return this._super(...arguments); + }, +}); diff --git a/website_sale_b2x_alt_price/static/tests/tours/b2b.esm.js b/website_sale_b2x_alt_price/static/tests/tours/b2b.esm.js new file mode 100644 index 0000000000..ddc7f4beca --- /dev/null +++ b/website_sale_b2x_alt_price/static/tests/tours/b2b.esm.js @@ -0,0 +1,134 @@ +/** @odoo-module */ + +/* Copyright 2020 Jairo Llopis - Tecnativa + * Copyright 2024 Carlos Lopez - Tecnativa + * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +import {registry} from "@web/core/registry"; + +const searchResultsPage = "/shop?search=website_sale_b2x_alt_price"; + +/** + * Go directly to the filtered products page. + * + * Going to the main products page can produce some false positives if + * there are demo products, and is slower because it means more steps and + * downloading many product images, so we direct the tour directly via JS. + */ +function goSearch() { + window.location = searchResultsPage; +} + +/** + * Test eCommerce in B2B mode. + * + * Remember fields meaning: + * + * - .oe_currency_value: main price, without taxes. + * - .text-muted.me-1.h6.mb-0: main price before discounts, without taxes(on products list). + * - .text-danger: main price before discounts, without taxes(on product Item). + * - .js_alt_price: alt price, with taxes. + * - .js_alt_list_price: alt price before discounts, with taxes. + */ +registry.category("web_tour.tours").add("website_sale_b2x_alt_price_b2b", { + test: true, + url: searchResultsPage, + steps: () => [ + // "Training on accounting" costs $100; no taxes, so no alt price + { + content: "select training on accounting product", + trigger: + ".oe_product_cart:not(:has(.js_alt_price)):has(.oe_currency_value:containsExact(100.00)) a:contains('Training on accounting')", + }, + { + content: "go back to search results", + trigger: + "#product_details:not(:has(.js_alt_price)):has(.oe_currency_value:containsExact(100.00)):contains('Training on accounting')", + run: goSearch, + }, + // Pen costs $5 + 22% tax + { + content: "select pen", + trigger: + ".oe_product_cart:has(.js_alt_price :containsExact(6.10)):has(.oe_currency_value:containsExact(5.00)) a:contains('Pen')", + }, + { + content: "go back to search results", + trigger: + "#product_details:has(.js_alt_price :containsExact(6.10)):has(.oe_currency_value:containsExact(5.00)):contains('Pen')", + run: goSearch, + }, + // Switch to "website_sale_b2x_alt_price discounted" pricelist + { + content: "open pricelist selector", + extra_trigger: + ".oe_product_cart:not(:has(.js_alt_list_price:visible, .text-danger:visible)) a:contains('Pen')", + trigger: ".btn:containsExact('website_sale_b2x_alt_price public')", + }, + { + content: "select website_sale_b2x_alt_price discounted", + trigger: + ".switcher_pricelist:containsExact('website_sale_b2x_alt_price discounted')", + }, + // Pen now has 10% discount + { + content: "select pen", + trigger: + ".oe_product_cart:has(.js_alt_list_price:visible :containsExact(6.10)):has(.js_alt_price :containsExact(5.49)):has(.text-muted.me-1.h6.mb-0 :containsExact(5.00)):has(.oe_currency_value:containsExact(4.50)) a:contains('Pen')", + }, + { + content: "go back to search results", + trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(6.10)):has(.js_alt_price :containsExact(5.49)):has(.text-danger :containsExact(5.00)):has(.oe_currency_value:containsExact(4.50)):contains('Pen')", + run: goSearch, + }, + // A5 Notebook costs $3 - 10% discount + 22% tax + { + content: "select notebook", + trigger: + ".oe_product_cart:has(.js_alt_list_price:visible :containsExact(3.66)):has(.js_alt_price :containsExact(3.29)):has(.text-muted.me-1.h6.mb-0 :containsExact(3.00)):has(.oe_currency_value:containsExact(2.70)) a:contains('Notebook')", + }, + // A4 Notebook costs $3.50 - 10% discount + 22% tax + { + content: "select variant: a4 size", + extra_trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(3.66)):has(.js_alt_price :containsExact(3.29)):has(.text-danger :containsExact(3.00)):has(.oe_currency_value:containsExact(2.70)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A4') :radio", + }, + { + content: "open pricelist selector", + extra_trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(4.27)):has(.js_alt_price :containsExact(3.84)):has(.text-danger :containsExact(3.50)):has(.oe_currency_value:containsExact(3.15)):contains('Notebook')", + trigger: ".btn:containsExact('website_sale_b2x_alt_price discounted')", + }, + // Change to "website_sale_b2x_alt_price public" pricelist; 10% discount disappears + { + content: "select website_sale_b2x_alt_price public", + trigger: + ".switcher_pricelist:containsExact('website_sale_b2x_alt_price public')", + }, + { + content: "select variant: a4 size", + // When changing pricelist, product was reset to Notebook A5 + extra_trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(3.66)):has(.oe_currency_value:containsExact(3.00)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A4') :radio", + }, + { + content: "select variant: a5 size", + extra_trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(4.27)):has(.oe_currency_value:containsExact(3.50)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A5') :radio", + }, + { + content: "check a5 price is fine", + trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(3.66)):has(.oe_currency_value:containsExact(3.00)):contains('Notebook')", + }, + ], +}); + +export default { + searchResultsPage: searchResultsPage, + goSearch: goSearch, +}; diff --git a/website_sale_b2x_alt_price/static/tests/tours/b2c.esm.js b/website_sale_b2x_alt_price/static/tests/tours/b2c.esm.js new file mode 100644 index 0000000000..99e4a9b152 --- /dev/null +++ b/website_sale_b2x_alt_price/static/tests/tours/b2c.esm.js @@ -0,0 +1,117 @@ +/** @odoo-module */ + +/* Copyright 2020 Jairo Llopis - Tecnativa + * Copyright 2024 Carlos Lopez - Tecnativa + * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +import {registry} from "@web/core/registry"; +import tour_b2b from "@website_sale_b2x_alt_price/../tests/tours/b2b.esm"; + +/** + * Test eCommerce in B2C mode. + * + * Remember fields meaning: + * + * - .oe_currency_value: main price, with taxes. + * - .text-muted.me-1.h6.mb-0: main price before discounts, without taxes(on products list). + * - .text-danger: main price before discounts, without taxes(on product Item). + * - .js_alt_price: alt price, without taxes. + * - .js_alt_list_price: alt price before discounts, without taxes. + */ +registry.category("web_tour.tours").add("website_sale_b2x_alt_price_b2c", { + test: true, + url: tour_b2b.searchResultsPage, + steps: () => [ + // "Training on accounting" costs $100; no taxes, so no alt price + { + content: "select training on accounting product", + trigger: + ".oe_product_cart:not(:has(.js_alt_price)):has(.oe_currency_value:containsExact(100.00)) a:contains('Training on accounting')", + }, + { + content: "go back to search results", + trigger: + "#product_details:not(:has(.js_alt_price)):has(.oe_currency_value:containsExact(100.00)):contains('Training on accounting')", + run: tour_b2b.goSearch, + }, + // Pen costs $5 + 22% tax + { + content: "select pen", + trigger: + ".oe_product_cart:has(.js_alt_price :containsExact(5.00)):has(.oe_currency_value:containsExact(6.10)) a:contains('Pen')", + }, + { + content: "go back to search results", + trigger: + "#product_details:has(.js_alt_price :containsExact(5.00)):has(.oe_currency_value:containsExact(6.10)):contains('Pen')", + run: tour_b2b.goSearch, + }, + // Switch to "website_sale_b2x_alt_price discounted" pricelist + { + content: "open pricelist selector", + extra_trigger: + ".oe_product_cart:not(:has(.js_alt_list_price:visible, .text-danger:visible)) a:contains('Pen')", + trigger: ".btn:containsExact('website_sale_b2x_alt_price public')", + }, + { + content: "select website_sale_b2x_alt_price discounted", + trigger: + ".switcher_pricelist:containsExact('website_sale_b2x_alt_price discounted')", + }, + // Pen now has 10% discount + { + content: "select pen", + trigger: + ".oe_product_cart:has(.js_alt_list_price:visible :containsExact(5.00)):has(.js_alt_price :containsExact(4.50)):has(.text-muted.me-1.h6.mb-0 :containsExact(6.10)):has(.oe_currency_value:containsExact(5.49)) a:contains('Pen')", + }, + { + content: "go back to search results", + trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(5.00)):has(.js_alt_price :containsExact(4.50)):has(.text-danger :containsExact(6.10)):has(.oe_currency_value:containsExact(5.49)):contains('Pen')", + run: tour_b2b.goSearch, + }, + // A5 Notebook costs $3 - 10% discount + 22% tax + { + content: "select notebook", + trigger: + ".oe_product_cart:has(.js_alt_list_price:visible :containsExact(3.00)):has(.js_alt_price :containsExact(2.70)):has(.text-muted.me-1.h6.mb-0 :containsExact(3.66)):has(.oe_currency_value:containsExact(3.29)) a:contains('Notebook')", + }, + // A4 Notebook costs $3.50 - 10% discount + 22% tax + { + content: "select variant: a4 size", + extra_trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(3.00)):has(.js_alt_price :containsExact(2.70)):has(.text-danger :containsExact(3.66)):has(.oe_currency_value:containsExact(3.29)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A4') :radio", + }, + { + content: "open pricelist selector", + extra_trigger: + "#product_details:has(.js_alt_list_price:visible :containsExact(3.50)):has(.js_alt_price :containsExact(3.15)):has(.text-danger :containsExact(4.27)):has(.oe_currency_value:containsExact(3.15)):contains('Notebook')", + trigger: ".btn:containsExact('website_sale_b2x_alt_price discounted')", + }, + // Change to "website_sale_b2x_alt_price public" pricelist; 10% discount disappears + { + content: "select website_sale_b2x_alt_price public", + trigger: + ".switcher_pricelist:containsExact('website_sale_b2x_alt_price public')", + }, + { + content: "select variant: a4 size", + // When changing pricelist, product was reset to Notebook A5 + extra_trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(3.00)):has(.oe_currency_value:containsExact(3.66)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A4') :radio", + }, + { + content: "select variant: a5 size", + extra_trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(3.50)):has(.oe_currency_value:containsExact(4.27)):contains('Notebook')", + trigger: ".js_attribute_value:contains('A5') :radio", + }, + { + content: "check a5 price is fine", + trigger: + "#product_details:not(:has(.js_alt_list_price:visible, .text-danger:visible)):has(.js_alt_price :containsExact(3.00)):has(.oe_currency_value:containsExact(3.66)):contains('Notebook')", + }, + ], +}); diff --git a/website_sale_b2x_alt_price/templates/product.xml b/website_sale_b2x_alt_price/templates/product.xml new file mode 100644 index 0000000000..37e999361a --- /dev/null +++ b/website_sale_b2x_alt_price/templates/product.xml @@ -0,0 +1,94 @@ + + + + + + + + diff --git a/website_sale_b2x_alt_price/tests/__init__.py b/website_sale_b2x_alt_price/tests/__init__.py new file mode 100644 index 0000000000..6dab214ac8 --- /dev/null +++ b/website_sale_b2x_alt_price/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ui diff --git a/website_sale_b2x_alt_price/tests/test_ui.py b/website_sale_b2x_alt_price/tests/test_ui.py new file mode 100644 index 0000000000..4044d3a676 --- /dev/null +++ b/website_sale_b2x_alt_price/tests/test_ui.py @@ -0,0 +1,171 @@ +# Copyright 2020 Jairo Llopis - Tecnativa +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo.tests.common import Form, HttpCase, tagged + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +@tagged("post_install", "-at_install") +class UICase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + # Create and select a pricelist + # to make tests pass no matter what l10n package is enabled + website = cls.env["website"].get_current_website() + pricelist = cls.env["product.pricelist"].create( + { + "name": "website_sale_b2x_alt_price public", + "currency_id": website.user_id.company_id.currency_id.id, + "selectable": True, + } + ) + website.user_id.property_product_pricelist = pricelist + admin = cls.env.ref("base.user_admin") + admin.property_product_pricelist = pricelist + # Create some demo taxes + cls.tax_group_22 = cls.env["account.tax.group"].create( + {"name": "Tax group 22%"} + ) + cls.tax_22_sale = cls.env["account.tax"].create( + { + "amount_type": "percent", + "amount": 22, + "description": "22%", + "name": "Tax sale 22%", + "tax_group_id": cls.tax_group_22.id, + "type_tax_use": "sale", + } + ) + cls.tax_22_purchase = cls.env["account.tax"].create( + { + "amount_type": "percent", + "amount": 22, + "description": "22%", + "name": "Tax purchase 22%", + "tax_group_id": cls.tax_group_22.id, + "type_tax_use": "purchase", + } + ) + # Create one product without taxes + cls.training_accounting = cls.env["product.template"].create( + { + "name": "Training on accounting - website_sale_b2x_alt_price", + "list_price": 100, + "type": "service", + "website_published": True, + "uom_id": cls.env.ref("uom.product_uom_unit").id, + "uom_po_id": cls.env.ref("uom.product_uom_unit").id, + "taxes_id": [(5, 0, 0)], + "supplier_taxes_id": [(5, 0, 0)], + } + ) + # One product with taxes + cls.pen = cls.env["product.template"].create( + { + "name": "Pen - website_sale_b2x_alt_price", + "list_price": 5, + "type": "consu", + "website_published": True, + "uom_id": cls.env.ref("uom.product_uom_unit").id, + "uom_po_id": cls.env.ref("uom.product_uom_unit").id, + "description_sale": "Best. Pen. Ever.", + "taxes_id": [(6, 0, [cls.tax_22_sale.id])], + "supplier_taxes_id": [(6, 0, [cls.tax_22_purchase.id])], + } + ) + # One product with taxes and variants + cls.notebook = cls.env["product.template"].create( + { + "name": "Notebook - website_sale_b2x_alt_price", + "list_price": 3, + "type": "consu", + "website_published": True, + "uom_id": cls.env.ref("uom.product_uom_unit").id, + "uom_po_id": cls.env.ref("uom.product_uom_unit").id, + "description_sale": "Best. Notebook. Ever.", + "taxes_id": [(6, 0, [cls.tax_22_sale.id])], + "supplier_taxes_id": [(6, 0, [cls.tax_22_purchase.id])], + } + ) + cls.sheet_size = cls.env["product.attribute"].create( + {"name": "Sheet size", "create_variant": "always"} + ) + cls.sheet_size_a4 = cls.env["product.attribute.value"].create( + {"name": "A4", "attribute_id": cls.sheet_size.id, "sequence": 20} + ) + cls.sheet_size_a5 = cls.env["product.attribute.value"].create( + {"name": "A5", "attribute_id": cls.sheet_size.id, "sequence": 10} + ) + cls.notebook_attline = cls.env["product.template.attribute.line"].create( + { + "product_tmpl_id": cls.notebook.id, + "attribute_id": cls.sheet_size.id, + "value_ids": [(6, 0, [cls.sheet_size_a4.id, cls.sheet_size_a5.id])], + } + ) + cls.notebook_size_a4 = cls.notebook_attline.product_template_value_ids[1] + cls.notebook_size_a5 = cls.notebook_attline.product_template_value_ids[0] + cls.notebook_a4 = cls.notebook._get_variant_for_combination( + cls.notebook_size_a4 + ) + cls.notebook_a4.write( + {"default_code": "NB_A4", "product_tmpl_id": cls.notebook.id} + ) + cls.notebook_a5 = cls.notebook._get_variant_for_combination( + cls.notebook_size_a5 + ) + cls.notebook_a5.write( + {"default_code": "NB_A5", "product_tmpl_id": cls.notebook.id} + ) + # A4 notebook is slightly more expensive + cls.notebook_a4.product_template_attribute_value_ids.price_extra = 0.5 + # Create a pricelist selectable from website with 10% discount + cls.discount_pricelist = cls.env["product.pricelist"].create( + { + "name": "website_sale_b2x_alt_price discounted", + "discount_policy": "without_discount", + "selectable": True, + "item_ids": [ + ( + 0, + 0, + { + "applied_on": "3_global", + "compute_price": "percentage", + "percent_price": 10, + }, + ), + ], + } + ) + + def _switch_tax_mode(self, mode): + assert mode in {"tax_excluded", "tax_included"} + config = Form(self.env["res.config.settings"]) + config.show_line_subtotals_tax_selection = mode + config.group_product_pricelist = True + config.product_pricelist_setting = "advanced" + config.group_discount_per_so_line = True + config = config.save() + config.execute() + + def test_ui_website_b2b(self): + """Test frontend b2b tour.""" + self._switch_tax_mode("tax_excluded") + self.start_tour( + "/shop?search=website_sale_b2x_alt_price", + "website_sale_b2x_alt_price_b2b", + login="admin", + ) + + def test_ui_website_b2c(self): + """Test frontend b2c tour.""" + self._switch_tax_mode("tax_included") + self.start_tour( + "/shop?search=website_sale_b2x_alt_price", + "website_sale_b2x_alt_price_b2c", + login="admin", + ) diff --git a/website_sale_suggest_create_account/__manifest__.py b/website_sale_suggest_create_account/__manifest__.py index fff7a03bcc..dcc1100510 100644 --- a/website_sale_suggest_create_account/__manifest__.py +++ b/website_sale_suggest_create_account/__manifest__.py @@ -13,7 +13,7 @@ "data": ["views/website_sale.xml"], "assets": { "web.assets_tests": [ - "website_sale_suggest_create_account/static/tests/tours/checkout.js", + "website_sale_suggest_create_account/static/tests/tours/checkout.esm.js", ], }, "post_init_hook": "post_init_hook", diff --git a/website_sale_suggest_create_account/static/tests/tours/checkout.esm.js b/website_sale_suggest_create_account/static/tests/tours/checkout.esm.js new file mode 100644 index 0000000000..28595697b5 --- /dev/null +++ b/website_sale_suggest_create_account/static/tests/tours/checkout.esm.js @@ -0,0 +1,47 @@ +/** @odoo-module */ + +import {registry} from "@web/core/registry"; +registry.category("web_tour.tours").add("shop_buy_checkout_suggest_account_website", { + test: true, + url: "/shop", + steps: () => [ + // Shop Page + { + trigger: "a:contains('Customizable')", + }, + { + trigger: "#add_to_cart", + }, + { + trigger: "button:contains('Proceed to Checkout')", + }, + // Cart page + { + trigger: + "a.btn-primary[href='/web/login?redirect=/shop/checkout?express=1']", + }, + // TODO: Add a step to check that "checkout" button doesn't exists + // Odoo 13.0 initial config doesn't have b2c actived for the website + // Login Page + { + trigger: "#login", + run: "text portal", + }, + { + trigger: "#password", + run: "text portal", + }, + { + trigger: "button.btn-primary:first", + }, + // Checkout Page + { + trigger: "button[name='o_payment_submit_button']", + }, + { + trigger: "span", + content: "Order", + }, + // The End + ], +}); diff --git a/website_sale_suggest_create_account/static/tests/tours/checkout.js b/website_sale_suggest_create_account/static/tests/tours/checkout.js deleted file mode 100644 index 49562e9721..0000000000 --- a/website_sale_suggest_create_account/static/tests/tours/checkout.js +++ /dev/null @@ -1,53 +0,0 @@ -odoo.define("wbesite_sale_suggest_create_account.shop_buy", function (require) { - "use strict"; - - var tour = require("web_tour.tour"); - - tour.register( - "shop_buy_checkout_suggest_account_website", - { - test: true, - url: "/shop", - }, - [ - // Shop Page - { - trigger: "a:contains('Customizable')", - }, - { - trigger: "#add_to_cart", - }, - { - trigger: "button:contains('Proceed to Checkout')", - }, - // Cart page - { - trigger: - "a.btn-primary[href='/web/login?redirect=/shop/checkout?express=1']", - }, - // TODO: Add a step to check that "checkout" button doesn't exists - // Odoo 13.0 initial config doesn't have b2c actived for the website - // Login Page - { - trigger: "#login", - run: "text portal", - }, - { - trigger: "#password", - run: "text portal", - }, - { - trigger: "button.btn-primary:first", - }, - // Checkout Page - { - trigger: "button[name='o_payment_submit_button']", - }, - { - trigger: "span", - content: "Order", - }, - // The End - ] - ); -});