From 3f158d7f63bf1dc16d2ad97fc9e76d0170943202 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 18 Jun 2021 17:19:06 +0200 Subject: [PATCH 01/32] [ADD] website_sale_product_pack: New module Compatibility module between sale_product_pack and website_sale TT30385 --- website_sale_product_pack/__init__.py | 1 + website_sale_product_pack/__manifest__.py | 14 ++ website_sale_product_pack/models/__init__.py | 2 + .../models/sale_order.py | 49 ++++++ website_sale_product_pack/models/website.py | 24 +++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 10 ++ website_sale_product_pack/readme/ROADMAP.rst | 3 + website_sale_product_pack/readme/USAGE.rst | 2 + .../src/js/website_sale_product_pack_tour.js | 160 ++++++++++++++++++ website_sale_product_pack/tests/__init__.py | 1 + .../tests/test_website_sale_product_pack.py | 101 +++++++++++ website_sale_product_pack/views/assets.xml | 11 ++ website_sale_product_pack/views/templates.xml | 144 ++++++++++++++++ 14 files changed, 525 insertions(+) create mode 100644 website_sale_product_pack/__init__.py create mode 100644 website_sale_product_pack/__manifest__.py create mode 100644 website_sale_product_pack/models/__init__.py create mode 100644 website_sale_product_pack/models/sale_order.py create mode 100644 website_sale_product_pack/models/website.py create mode 100644 website_sale_product_pack/readme/CONTRIBUTORS.rst create mode 100644 website_sale_product_pack/readme/DESCRIPTION.rst create mode 100644 website_sale_product_pack/readme/ROADMAP.rst create mode 100644 website_sale_product_pack/readme/USAGE.rst create mode 100644 website_sale_product_pack/static/src/js/website_sale_product_pack_tour.js create mode 100644 website_sale_product_pack/tests/__init__.py create mode 100644 website_sale_product_pack/tests/test_website_sale_product_pack.py create mode 100644 website_sale_product_pack/views/assets.xml create mode 100644 website_sale_product_pack/views/templates.xml diff --git a/website_sale_product_pack/__init__.py b/website_sale_product_pack/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/website_sale_product_pack/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/website_sale_product_pack/__manifest__.py b/website_sale_product_pack/__manifest__.py new file mode 100644 index 000000000..afbcd26fa --- /dev/null +++ b/website_sale_product_pack/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Website Sale Product Pack", + "category": "E-Commerce", + "summary": "Compatibility module of product pack with e-commerce", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "depends": ["website_sale", "sale_product_pack"], + "data": ["views/assets.xml", "views/templates.xml"], + "author": "Tecnativa, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-pack", + "installable": True, +} diff --git a/website_sale_product_pack/models/__init__.py b/website_sale_product_pack/models/__init__.py new file mode 100644 index 000000000..71db68c22 --- /dev/null +++ b/website_sale_product_pack/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order +from . import website diff --git a/website_sale_product_pack/models/sale_order.py b/website_sale_product_pack/models/sale_order.py new file mode 100644 index 000000000..a30fdbb29 --- /dev/null +++ b/website_sale_product_pack/models/sale_order.py @@ -0,0 +1,49 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _cart_update( + self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs + ): + """We need to keep the discount defined on the components when checking out""" + line = self.env["sale.order.line"].browse(line_id) + if line and line.pack_parent_line_id: + return super( + SaleOrder, self.with_context(pack_discount=line.discount) + )._cart_update(product_id, line_id, add_qty, set_qty, **kwargs) + return super()._cart_update(product_id, line_id, add_qty, set_qty, **kwargs) + + @api.depends("order_line.product_uom_qty", "order_line.product_id") + def _compute_cart_info(self): + """We only want to count the main pack line, not the component lines""" + super()._compute_cart_info() + for order in self: + order.cart_quantity = int( + sum( + order.website_order_line.filtered( + lambda x: not x.pack_parent_line_id + ).mapped("product_uom_qty") + ) + ) + + def _website_product_id_change(self, order_id, product_id, qty=0): + """In the final checkout step, we could miss the component discount as the + product prices are recomputed""" + res = super()._website_product_id_change(order_id, product_id, qty=qty) + if self.env.context.get("pack_discount"): + res["discount"] = self.env.context.get("pack_discount") + return res + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def unlink(self): + """The website calls this method specifically. We want to get rid of + the children lines so the user doesn't have to""" + join_pack_children = self + self.mapped("pack_child_line_ids") + return super(SaleOrderLine, join_pack_children.exists()).unlink() diff --git a/website_sale_product_pack/models/website.py b/website_sale_product_pack/models/website.py new file mode 100644 index 000000000..19eb2368f --- /dev/null +++ b/website_sale_product_pack/models/website.py @@ -0,0 +1,24 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class Website(models.Model): + _inherit = "website" + + def sale_get_order( + self, + force_create=False, + code=None, + update_pricelist=False, + force_pricelist=False, + ): + """Communicate with product pack expansion method to check if it's necessary + to expand the product pack lines or not via context""" + if update_pricelist: + return super( + Website, self.with_context(update_pricelist=True) + ).sale_get_order(force_create, code, update_pricelist, force_pricelist) + return super().sale_get_order( + force_create, code, update_pricelist, force_pricelist + ) diff --git a/website_sale_product_pack/readme/CONTRIBUTORS.rst b/website_sale_product_pack/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..94b6ba953 --- /dev/null +++ b/website_sale_product_pack/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Tecnativa `_: + + * David Vidal diff --git a/website_sale_product_pack/readme/DESCRIPTION.rst b/website_sale_product_pack/readme/DESCRIPTION.rst new file mode 100644 index 000000000..98f90f7c1 --- /dev/null +++ b/website_sale_product_pack/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module introduces compatibility of product packs with e-commerce: + +- In the cart summary, the components aren't editable and are shown hanging + from the main pack reference. +- When we remove a pack from the cart, their components are removed as well. +- The cart popup summary only shows the main pack line and discards the sublines in + the units count. +- The cart summary shows the component lines hanging from the main one as well. +- It's ensured the the prices are shown correctly for the whole pack and that they're + correctly summarized depending on the pack type. diff --git a/website_sale_product_pack/readme/ROADMAP.rst b/website_sale_product_pack/readme/ROADMAP.rst new file mode 100644 index 000000000..03a36ff32 --- /dev/null +++ b/website_sale_product_pack/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* Improve pack cart display. +* When we have subpacks (a pack inside a pack) we should improve visually how + it's shown in the cart. diff --git a/website_sale_product_pack/readme/USAGE.rst b/website_sale_product_pack/readme/USAGE.rst new file mode 100644 index 000000000..542600300 --- /dev/null +++ b/website_sale_product_pack/readme/USAGE.rst @@ -0,0 +1,2 @@ +There are several demo packs to test the module. Publish them and add them to the cart +from the frontend. You should have the same quotation as if you do it in the backend. diff --git a/website_sale_product_pack/static/src/js/website_sale_product_pack_tour.js b/website_sale_product_pack/static/src/js/website_sale_product_pack_tour.js new file mode 100644 index 000000000..6dfb36316 --- /dev/null +++ b/website_sale_product_pack/static/src/js/website_sale_product_pack_tour.js @@ -0,0 +1,160 @@ +/* Copyright 2021 Tecnativa - David Vidal + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ + +odoo.define( + "website_sale_product_pack.tour_create_components_price_order_line", + function(require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: + "a:contains('Pack CPU (Detailed - Displayed Components Price)')", + }, + { + trigger: "a:contains('Add to Cart')", + }, + { + trigger: "a:contains('Process Checkout')", + }, + { + trigger: "a[href='/shop']", + }, + ]; + tour.register( + "create_components_price_order_line", + { + url: "/shop", + test: true, + }, + steps + ); + } +); + +odoo.define("website_sale_product_pack.tour_create_ignored_price_order_line", function( + require +) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: "a:contains('Pack CPU (Detailed - Ignored Components Price)')", + }, + { + trigger: "a:contains('Add to Cart')", + }, + { + trigger: "a:contains('Process Checkout')", + }, + { + trigger: "a[href='/shop']", + }, + ]; + tour.register( + "create_ignored_price_order_line", + { + url: "/shop", + test: true, + }, + steps + ); +}); + +odoo.define( + "website_sale_product_pack.tour_create_totalized_price_order_line", + function(require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: + "a:contains('Pack CPU (Detailed - Totalized Components Price)')", + }, + { + trigger: "a:contains('Add to Cart')", + }, + { + trigger: "a:contains('Process Checkout')", + }, + { + trigger: "a[href='/shop']", + }, + ]; + tour.register( + "create_totalized_price_order_line", + { + url: "/shop", + test: true, + }, + steps + ); + } +); + +odoo.define( + "website_sale_product_pack.tour_create_non_detailed_price_order_line", + function(require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: "a:contains('Non Detailed - Totalized Components Price')", + }, + { + trigger: "a:contains('Add to Cart')", + }, + { + trigger: "a:contains('Process Checkout')", + }, + { + trigger: "a[href='/shop']", + }, + ]; + tour.register( + "create_non_detailed_price_order_line", + { + url: "/shop", + test: true, + }, + steps + ); + } +); + +odoo.define("website_sale_product_pack.tour_update_pack_qty", function(require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: "a:contains('Pack CPU (Detailed - Displayed Components Price)')", + }, + { + trigger: "a:contains('Add to Cart')", + }, + { + trigger: "a:contains('Process Checkout')", + }, + { + trigger: "a[href='/shop']", + }, + ]; + tour.register( + "update_pack_qty", + { + url: "/shop", + test: true, + }, + steps + ); +}); diff --git a/website_sale_product_pack/tests/__init__.py b/website_sale_product_pack/tests/__init__.py new file mode 100644 index 000000000..05aebdc1c --- /dev/null +++ b/website_sale_product_pack/tests/__init__.py @@ -0,0 +1 @@ +from . import test_website_sale_product_pack diff --git a/website_sale_product_pack/tests/test_website_sale_product_pack.py b/website_sale_product_pack/tests/test_website_sale_product_pack.py new file mode 100644 index 000000000..f8fd8d53b --- /dev/null +++ b/website_sale_product_pack/tests/test_website_sale_product_pack.py @@ -0,0 +1,101 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests.common import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class WebsiteSaleHttpCase(HttpCase): + def setUp(self): + super().setUp() + self.user_portal = self.env.ref("base.demo_user0") + self.product_pdc = self.env.ref( + "product_pack.product_pack_cpu_detailed_components" + ) + self.product_pdi = self.env.ref( + "product_pack.product_pack_cpu_detailed_ignored" + ) + self.product_pdt = self.env.ref( + "product_pack.product_pack_cpu_detailed_totalized" + ) + self.product_pnd = self.env.ref("product_pack.product_pack_cpu_non_detailed") + self.packs = ( + self.product_pdc + self.product_pdi + self.product_pdt + self.product_pnd + ) + # Publish the products and put them in the first results + self.packs.write({"website_published": True, "website_sequence": 0}) + + def _get_component_prices_sum(self, product_pack): + component_prices = 0.0 + for pack_line in product_pack.get_pack_lines(): + product_line_price = pack_line.product_id.list_price * ( + 1 - (pack_line.sale_discount or 0.0) / 100.0 + ) + component_prices += product_line_price * pack_line.quantity + return component_prices + + def test_create_components_price_order_line(self): + """Test with the same premise that in sale_product_pack but in a + frontend tour""" + self.start_tour("/shop", "create_components_price_order_line", login="portal") + sale = self.env["sale.order"].search([], limit=1) + # After create, there will be four lines + self.assertEqual(len(sale.order_line), 4) + # The products of those four lines are the main product pack and its + # product components + self.assertEqual( + sale.order_line.mapped("product_id"), + self.product_pdc | self.product_pdc.get_pack_lines().mapped("product_id"), + ) + + def test_create_ignored_price_order_line(self): + """Test with the same premise that in sale_product_pack but in a frontend + tour""" + self.start_tour("/shop", "create_ignored_price_order_line", login="portal") + sale = self.env["sale.order"].search([], limit=1) + line = sale.order_line.filtered(lambda x: x.product_id == self.product_pdi) + # After create, there will be four lines + self.assertEqual(len(sale.order_line), 4) + # The products of those four lines are the main product pack and its + # product components + self.assertEqual( + sale.order_line.mapped("product_id"), + self.product_pdi | self.product_pdi.get_pack_lines().mapped("product_id"), + ) + # All component lines have zero as subtotal + self.assertEqual((sale.order_line - line).mapped("price_subtotal"), [0, 0, 0]) + # Pack price is different from the sum of component prices + self.assertEqual(line.price_subtotal, 30.75) + self.assertNotEqual(self._get_component_prices_sum(self.product_pdi), 30.75) + + def test_create_totalized_price_order_line(self): + """Test with the same premise that in sale_product_pack but in a frontend tour + with a detailed totalized pack""" + self.start_tour("/shop", "create_totalized_price_order_line", login="portal") + sale = self.env["sale.order"].search([], limit=1) + line = sale.order_line.filtered(lambda x: x.product_id == self.product_pdt) + # After create, there will be four lines + self.assertEqual(len(sale.order_line), 4) + # The products of those four lines are the main product pack and its + # product components + self.assertEqual( + sale.order_line.mapped("product_id"), + self.product_pdt | self.product_pdt.get_pack_lines().mapped("product_id"), + ) + # All component lines have zero as subtotal + self.assertEqual((sale.order_line - line).mapped("price_subtotal"), [0, 0, 0]) + # Pack price is equal to the sum of component prices + self.assertEqual(line.price_subtotal, 2662.5) + self.assertEqual(self._get_component_prices_sum(self.product_pdt), 2662.5) + + def test_create_non_detailed_price_order_line(self): + """Test with the same premise that in sale_product_pack but in a frontend + tour""" + self.start_tour("/shop", "create_non_detailed_price_order_line", login="portal") + sale = self.env["sale.order"].search([], limit=1) + line = sale.order_line.filtered(lambda x: x.product_id == self.product_pnd) + # After create, there will be only one line, because product_type is + # not a detailed one + self.assertEqual(len(line), 1) + # Pack price is equal to the sum of component prices + self.assertEqual(line.price_subtotal, 2662.5) + self.assertEqual(self._get_component_prices_sum(self.product_pnd), 2662.5) diff --git a/website_sale_product_pack/views/assets.xml b/website_sale_product_pack/views/assets.xml new file mode 100644 index 000000000..e1c876c0a --- /dev/null +++ b/website_sale_product_pack/views/assets.xml @@ -0,0 +1,11 @@ + + +