diff --git a/setup/website_sale_cart_quantity_shop/odoo/addons/website_sale_cart_quantity_shop b/setup/website_sale_cart_quantity_shop/odoo/addons/website_sale_cart_quantity_shop new file mode 120000 index 0000000000..7dc113e740 --- /dev/null +++ b/setup/website_sale_cart_quantity_shop/odoo/addons/website_sale_cart_quantity_shop @@ -0,0 +1 @@ +../../../../website_sale_cart_quantity_shop \ No newline at end of file diff --git a/setup/website_sale_cart_quantity_shop/setup.py b/setup/website_sale_cart_quantity_shop/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/website_sale_cart_quantity_shop/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/website_sale_cart_quantity_shop/README.rst b/website_sale_cart_quantity_shop/README.rst new file mode 100644 index 0000000000..b444942c4b --- /dev/null +++ b/website_sale_cart_quantity_shop/README.rst @@ -0,0 +1,59 @@ +.. image:: https://pbs.twimg.com/profile_images/547133733149483008/0JKHr3Av_400x400.png + :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +Website Sale Cart Quantity Shop +======================================= + +- **Plus and Minus Buttons**: + - Users can increase or decrease the quantity of a product by clicking the plus (+) or minus (−) buttons next to the quantity input field. + - Clicking the plus button increments the quantity by 1. + - Clicking the minus button decrements the quantity by 1, with a minimum value of 0. + +- **Direct Input**: + - Users can manually enter the desired quantity directly into the input box in the middle of the buttons. + - The input field validates and updates the quantity based on user input. + +- **Dynamic Updates**: + - The quantity input field dynamically updates the cart when the quantity is changed using either the buttons or by direct input. + - The system ensures that the quantity displayed is in sync with the quantity available in the cart. + +- **Visual Feedback**: + - The input field's background color and text color change when the quantity matches the available stock, providing visual feedback to the user. + +Usage +===== + +1. **Navigate to the Shop Page**: + - Go to your shop or category page in the Odoo eCommerce interface. + +2. **Adjust Product Quantity**: + - Use the plus (+) button to increase the quantity by 1. + - Use the minus (−) button to decrease the quantity by 1, ensuring the quantity does not drop below 0. + - Enter a specific quantity directly into the input field to set the desired amount. + +3. **Visual Feedback**: + - Observe changes in the input field color to reflect the available stock. + +Configuration +============= + +No additional configuration is required. The module integrates seamlessly with the existing product quantity functionality on the shop page. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. If you encounter any issues, please check there to see if your issue has already been reported. If not, provide detailed feedback to help us resolve it. + +Credits +======= + +Contributors +------------ +* Unai Beristain + +Do not contact contributors directly about support or help with technical issues. + +License +======= +This project is licensed under the AGPL-3 License. For more details, please refer to the LICENSE file or visit . diff --git a/website_sale_cart_quantity_shop/__init__.py b/website_sale_cart_quantity_shop/__init__.py new file mode 100644 index 0000000000..fef015afb6 --- /dev/null +++ b/website_sale_cart_quantity_shop/__init__.py @@ -0,0 +1,5 @@ +############################################################################### +# For copyright and license notices, see __manifest__.py file in root directory +############################################################################### +from .hooks import pre_init_hook +from . import controllers diff --git a/website_sale_cart_quantity_shop/__manifest__.py b/website_sale_cart_quantity_shop/__manifest__.py new file mode 100644 index 0000000000..61438928bb --- /dev/null +++ b/website_sale_cart_quantity_shop/__manifest__.py @@ -0,0 +1,24 @@ +# Ooops +# Cetmix +# Copyright 2024 Unai Beristain - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Website Sale Cart Quantity Shop", + "summary": "Choose cart quantity from shop page", + "category": "Website", + "version": "14.0.1.1.0", + "author": "AvanzOSC, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/e-commerce", + "license": "AGPL-3", + "depends": [ + "stock", + "website_sale", + ], + "data": [ + "views/assets.xml", + "views/website_sale.xml", + ], + "installable": True, + "pre_init_hook": "pre_init_hook", +} diff --git a/website_sale_cart_quantity_shop/controllers/__init__.py b/website_sale_cart_quantity_shop/controllers/__init__.py new file mode 100644 index 0000000000..1b65f0ff11 --- /dev/null +++ b/website_sale_cart_quantity_shop/controllers/__init__.py @@ -0,0 +1,4 @@ +############################################################################### +# For copyright and license notices, see __manifest__.py file in root directory +############################################################################### +from . import website_sale diff --git a/website_sale_cart_quantity_shop/controllers/website_sale.py b/website_sale_cart_quantity_shop/controllers/website_sale.py new file mode 100644 index 0000000000..230d455663 --- /dev/null +++ b/website_sale_cart_quantity_shop/controllers/website_sale.py @@ -0,0 +1,93 @@ +# Copyright 2024 Unai Beristain - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import json + +from odoo import fields +from odoo.http import request, route + +from odoo.addons.website_sale.controllers.main import WebsiteSaleForm + + +class WebsiteSaleForm(WebsiteSaleForm): + @route( + ["/shop/cart/update_json_from_shop"], + type="json", + auth="public", + methods=["POST"], + website=True, + csrf=False, + ) + def cart_update_json_from_shop( + self, product_id, line_id=None, add_qty=1, set_qty=0, display=True, **kw + ): + + sale_order = request.website.sale_get_order(force_create=True) + + if sale_order.state != "draft": + request.session["sale_order_id"] = None + sale_order = request.website.sale_get_order(force_create=True) + + product_custom_attribute_values = None + if kw.get("product_custom_attribute_values"): + product_custom_attribute_values = json.loads( + kw.get("product_custom_attribute_values") + ) + + no_variant_attribute_values = None + if kw.get("no_variant_attribute_values"): + no_variant_attribute_values = json.loads( + kw.get("no_variant_attribute_values") + ) + + value = sale_order._cart_update( + product_id=product_id, + line_id=line_id, + add_qty=add_qty, + set_qty=set_qty, + product_custom_attribute_values=product_custom_attribute_values, + no_variant_attribute_values=no_variant_attribute_values, + ) + value["cart_quantity"] = sale_order.cart_quantity + if not sale_order.cart_quantity: + request.website.sale_reset() + return value + + if not display: + return value + + value["website_sale.cart_lines"] = request.env["ir.ui.view"]._render_template( + "website_sale.cart_lines", + { + "website_sale_order": sale_order, + "date": fields.Date.today(), + "suggested_products": sale_order._cart_accessories(), + }, + ) + value["website_sale.short_cart_summary"] = request.env[ + "ir.ui.view" + ]._render_template( + "website_sale.short_cart_summary", + { + "website_sale_order": sale_order, + }, + ) + + order_line = ( + sale_order.sudo().order_line.filtered( + lambda line: line.product_id.id == product_id + ) + if sale_order and sale_order.order_line + else [] + ) + + value["product_cart_qty"] = ( + int(order_line[0].sudo().product_uom_qty) + if order_line and order_line[0].product_uom_qty + else 0 + ) + + product = request.env["product.product"].sudo().browse(product_id) + value["product_available_qty"] = product.qty_available - product.outgoing_qty + + return value diff --git a/website_sale_cart_quantity_shop/hooks.py b/website_sale_cart_quantity_shop/hooks.py new file mode 100644 index 0000000000..22f98c3df1 --- /dev/null +++ b/website_sale_cart_quantity_shop/hooks.py @@ -0,0 +1,14 @@ +# Copyright 2024 Unai Beristain - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +# Add to Cart button on product page +# Necessary to know product_id to add to cart +def pre_init_hook(cr): + cr.execute( + """ + UPDATE ir_ui_view + SET active = TRUE + WHERE key = 'website_sale.products_add_to_cart' + """ + ) diff --git a/website_sale_cart_quantity_shop/static/src/js/recalculate_product_qty.js b/website_sale_cart_quantity_shop/static/src/js/recalculate_product_qty.js new file mode 100644 index 0000000000..6b817e20ef --- /dev/null +++ b/website_sale_cart_quantity_shop/static/src/js/recalculate_product_qty.js @@ -0,0 +1,179 @@ +// Copyright 2024 Unai Beristain - AvanzOSC +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +odoo.define("website_sale_cart_quantity_shop.recalculate_product_qty", function ( + require +) { + "use strict"; + + const publicWidget = require("web.public.widget"); + const wSaleUtils = require("website_sale.utils"); + + // Define a flag to track if an RPC call is in progress + let rpcInProgress = false; + + function isCategoryPage() { + const url = window.location.href; + return ( + url.includes("/category/") || + (url.includes("/shop") && !url.includes("/shop/")) || + url.includes("/shop/page/") + ); + } + + if (isCategoryPage()) { + $(document).ready(function () { + $("input.form-control.quantity").each(function () { + var newValue = parseInt($(this).val().replace(",", "."), 10) || 0; + $(this).val(newValue); + $(this).data("oldValue", newValue); + }); + }); + + publicWidget.registry.WebsiteSale.include({ + custom_add_qty: 0, + changeTriggeredByButton: false, + modifiedInputField: null, + oldValue: 0, + newValue: 0, + + start: function () { + this._super.apply(this, arguments); + var self = this; + + $(".fa.fa-plus") + .parent() + .click(function (event) { + event.preventDefault(); + var inputField = $(this) + .parent() + .siblings("input.form-control.quantity"); + self.modifiedInputField = inputField; + self.oldValue = parseInt(inputField.val(), 10) || 0; + self.newValue = self.oldValue + 1; + inputField.data("oldValue", self.newValue); + self.custom_add_qty = 1; + self.changeTriggeredByButton = true; + self._onClickAdd(event); + }); + + $(".fa.fa-minus") + .parent() + .click(function (event) { + event.preventDefault(); + var inputField = $(this) + .parent() + .siblings("input.form-control.quantity"); + self.modifiedInputField = inputField; + self.oldValue = parseInt(inputField.val(), 10) || 0; + self.newValue = Math.max(self.oldValue - 1, 0); + inputField.data("oldValue", self.newValue); + self.custom_add_qty = -1; + self.changeTriggeredByButton = true; + self._onClickAdd(event); + }); + + $("input.form-control.quantity").on("change", function (event) { + self.modifiedInputField = $(this); + + if (self.changeTriggeredByButton) { + self.changeTriggeredByButton = false; + } else { + self.oldValue = $(this).data("oldValue") || 0; + self.newValue = + parseInt($(this).val().replace(",", "."), 10) || 0; + + if (self.newValue < 0 || isNaN(self.newValue)) { + self.newValue = 0; + } + + $(this).val(self.newValue); + $(this).data("oldValue", self.newValue); + self.custom_add_qty = self.newValue - self.oldValue; + self._onClickAdd(event); + } + }); + }, + + _onClickAdd: function (ev) { + this.isDynamic = true; + this.pageType = $(ev.currentTarget).data("page-type"); + this.targetEl = $(ev.currentTarget); + this._super.apply(this, arguments); + }, + + _submitForm: function () { + if (rpcInProgress) { + console.log( + "An RPC call is already in progress. Skipping this call." + ); + return Promise.resolve(); + } + + rpcInProgress = true; + + const self = this; + const params = this.rootProduct; + params.add_qty = this.custom_add_qty; + + const $inputField = this.modifiedInputField; + + params.product_custom_attribute_values = JSON.stringify( + params.product_custom_attribute_values + ); + params.no_variant_attribute_values = JSON.stringify( + params.no_variant_attribute_values + ); + + if (this.isBuyNow) { + params.express = true; + } + + return this._rpc({ + route: "/shop/cart/update_json_from_shop", + params: params, + }) + .then((data) => { + self.oldValue = parseInt($inputField.val(), 10) || 0; + self.newValue = parseInt(data.product_cart_qty, 10) || 0; + $inputField.data("oldValue", self.newValue); + $inputField.val(self.newValue); + self.changeTriggeredByButton = true; + + if ( + data.product_cart_qty == data.product_available_qty && + data.product_cart_qty != 0 + ) { + $inputField.css({ + color: "white", + "background-color": "black", + "font-weight": "bold", + }); + } else { + $inputField.css({ + color: "black", + "background-color": "white", + "font-weight": "normal", + }); + } + + wSaleUtils.updateCartNavBar(data); + const $navButton = $("header .o_wsale_my_cart").parent(); + let el = $(); + if (self.pageType === "product") { + el = $("#o-carousel-product"); + } + if (self.pageType === "products") { + el = self.targetEl.parents(".o_wsale_product_grid_wrapper"); + } + wSaleUtils.animateClone($navButton, el, 25, 40); + + rpcInProgress = false; + }) + .catch((error) => { + console.error("Error occurred during RPC call:", error); + rpcInProgress = false; + }); + }, + }); + } +}); diff --git a/website_sale_cart_quantity_shop/views/assets.xml b/website_sale_cart_quantity_shop/views/assets.xml new file mode 100644 index 0000000000..2f6c32298f --- /dev/null +++ b/website_sale_cart_quantity_shop/views/assets.xml @@ -0,0 +1,14 @@ + +