diff --git a/README.md b/README.md index 542a1dec12..30b643cda1 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Available addons addon | version | maintainers | summary --- | --- | --- | --- [website_sale_b2x_alt_price](website_sale_b2x_alt_price/) | 17.0.1.0.0 | Yajo | Display prices with(out) taxes in eCommerce, complementing normal mode +[website_sale_cart_add_product_xlsx_csv](website_sale_cart_add_product_xlsx_csv/) | 17.0.1.0.0 | | Adds button to import xlsx or csv in website cart [website_sale_checkout_skip_payment](website_sale_checkout_skip_payment/) | 17.0.1.0.1 | | Skip payment for logged users in checkout process [website_sale_empty_cart](website_sale_empty_cart/) | 17.0.1.0.0 | | Adds a button in the website cart to empty all [website_sale_hide_empty_category](website_sale_hide_empty_category/) | 17.0.1.0.0 | | Hide any Product Categories that are empty diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..bf99963586 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +openpyxl diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 60d4c5ca63..6e2efb33bd 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,8 +1,9 @@ [project] name = "odoo-addons-oca-e-commerce" -version = "17.0.20251124.0" +version = "17.0.20260320.0" dependencies = [ "odoo-addon-website_sale_b2x_alt_price>=17.0dev,<17.1dev", + "odoo-addon-website_sale_cart_add_product_xlsx_csv>=17.0dev,<17.1dev", "odoo-addon-website_sale_checkout_skip_payment>=17.0dev,<17.1dev", "odoo-addon-website_sale_empty_cart>=17.0dev,<17.1dev", "odoo-addon-website_sale_hide_empty_category>=17.0dev,<17.1dev", diff --git a/website_sale_cart_add_product_xlsx_csv/README.rst b/website_sale_cart_add_product_xlsx_csv/README.rst new file mode 100644 index 0000000000..3de984e64b --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/README.rst @@ -0,0 +1,124 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +====================================== +Website Sale Cart Add Product Xlsx Csv +====================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:79fce4d16c2c43bfb7623e87685ea7b9d1385305f4a70f4cf365bc2ccea0ebb8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-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_cart_add_product_xlsx_csv + :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_cart_add_product_xlsx_csv + :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 adds a button in the website cart that let users import a +xlsx or csv file with the products and quantities that he wants to add. + +This button is only shown on empty carts, and the file must contain the +product's internal reference and the quantity to buy. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +- To enable the cart import button, go to Website / Configuration / + Settings. Then search the "Cart Import Button" and set its value. +- To edit the maximum file size, go to Website / Configuration / + Settings. Then search the "Cart import button file size limit" and + edit its value. + +Usage +===== + +To use this module, you need to: + +1. Go to the "/shop/cart" path of your server website. Example: + "http://localhost:8069/shop/cart" +2. You will see your cart. Empty it if it has products. +3. Click on "Download example file". Fill the xlsx file rows: 3.1. The + "default_code" column is the internal reference of the product that + you are going to add to your cart 3.2. The "product_uom_qty" is the + qty of the product that you are going to add to your cart +4. Select the filled file, and click the "Import" button +5. You will see the cart with the imported products. If some error + hapened or there are products that could not be imported, the system + will show a message + +Known issues / Roadmap +====================== + +- Despithe the fact that the cart button is enabled by default, some + Odoo instances have the website configured to hide it if it is empty. + If the shopping cart button is hidden without anything in it, the + functionality cannot be used until something is added to the cart. + +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 +------- + +* Sygel + +Contributors +------------ + +- `Sygel `__: + + - Alberto Martínez + - Valentin Vinagre + - Harald Panten + +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. + +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_cart_add_product_xlsx_csv/__init__.py b/website_sale_cart_add_product_xlsx_csv/__init__.py new file mode 100644 index 0000000000..5c2bd8c997 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import controllers +from . import models diff --git a/website_sale_cart_add_product_xlsx_csv/__manifest__.py b/website_sale_cart_add_product_xlsx_csv/__manifest__.py new file mode 100644 index 0000000000..333e31a00b --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2025 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Website Sale Cart Add Product Xlsx Csv", + "summary": "Adds button to import xlsx or csv in website cart", + "version": "17.0.1.0.0", + "website": "https://github.com/OCA/e-commerce", + "author": "Sygel, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "website_sale_stock", + ], + "external_dependencies": {"python": ["openpyxl"]}, + "data": [ + "data/import_file_example.xml", + "views/cart_templates.xml", + "views/res_config_settings.xml", + ], +} diff --git a/website_sale_cart_add_product_xlsx_csv/controllers/__init__.py b/website_sale_cart_add_product_xlsx_csv/controllers/__init__.py new file mode 100644 index 0000000000..f43232f012 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/controllers/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import main diff --git a/website_sale_cart_add_product_xlsx_csv/controllers/main.py b/website_sale_cart_add_product_xlsx_csv/controllers/main.py new file mode 100644 index 0000000000..c767b61f2b --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/controllers/main.py @@ -0,0 +1,178 @@ +# Copyright 2025 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import csv +import io +import math + +from openpyxl import load_workbook + +from odoo import _, http +from odoo.exceptions import UserError +from odoo.http import content_disposition, request + +from odoo.addons.website_sale.controllers.main import WebsiteSale + + +class WebsiteSaleAddProductXlsxCsv(WebsiteSale): + def _parse_file(self, file): + headers = False + data = False + if file.filename.endswith(".csv"): + file.stream.seek(0) + text_stream = io.StringIO(file.read().decode("utf-8")) + reader = csv.DictReader(text_stream) + headers = reader.fieldnames + rows = [row for row in reader if any(row.values())] + data = enumerate(rows, start=2) + data = rows + + elif file.filename.endswith(".xlsx"): + file.stream.seek(0) + wb = load_workbook(io.BytesIO(file.read()), data_only=True) + sheet = wb.active + rows = list(sheet.iter_rows(values_only=True)) + headers = [str(h).strip() for h in rows[0]] + rows = rows[1:] + data = [dict(zip(headers, row, strict=True)) for row in rows if any(row)] + + return headers, data + + def _can_be_imported(self, product, qty, sale): + can_be_imported = True + error_reason = "" + try: + qty = float(qty) + except ValueError: + can_be_imported = False + error_reason = _("The quantity is not a number") + if not isinstance(qty, int | float) or math.isnan(qty) or qty <= 0: + can_be_imported = False + error_reason = _("The quantity should be a positive number") + if len(product) != 1: + can_be_imported = False + error_reason = _("The product was not found") + elif not product.active: + can_be_imported = False + error_reason = _("The product is archived") + elif not product.sale_ok: + can_be_imported = False + error_reason = _("Product can not be sold") + elif not product.website_published: + can_be_imported = False + error_reason = _("Product is not published on the website") + elif ( + not product.allow_out_of_stock_order + and qty > product.with_context(warehouse=sale.warehouse_id.id).free_qty + ): + can_be_imported = False + qty = product.with_context(warehouse=sale.warehouse_id.id).free_qty + error_reason = _("Product max saleable qty is {}").format(qty) + return can_be_imported, error_reason + + def _check_file_size(self, file): + file_limit_mb = request.website.cart_import_button_file_limit + file_size_bytes = file.stream.seek(0, 2) + file_size_ok = file_size_bytes / 1024**2 <= file_limit_mb + file.stream.seek(0) + return file_size_ok + + def import_file(self, sale_order, file): + import_status = "sucess" + import_msg = "" + headers = data = False + failed_products = [] + if not self._check_file_size(file): + import_status = "error" + import_msg = _( + "The file size is greater than the maximum allowed, {} MB" + ).format(request.website.cart_import_button_file_limit) + + if import_status != "error" and not ( + file.filename.endswith(".csv") or file.filename.endswith(".xlsx") + ): + import_status = "error" + import_msg = _("Incorrect file format, it must me a .xlsx or a .csv") + else: + headers, data = self._parse_file(file) + + if import_status != "error" and headers != ["default_code", "product_uom_qty"]: + import_status = "error" + import_msg = _( + "Incorrect file format, " + "the columns should be 'default_code' and 'product_uom_qty'" + ) + + if import_status != "error": + for index, row in enumerate(data, start=2): + default_code = str(row["default_code"]).strip() + qty = row["product_uom_qty"] + product = ( + request.env["product.product"] + .with_context(active_test=False) + .search([("default_code", "=", default_code)]) + ) + can_be_imported, warn_msg = self._can_be_imported( + product, qty, sale_order + ) + if not can_be_imported: + import_status = "warn" + failed_products.append( + f"Line {index}. {row['default_code']}: {warn_msg}" + ) + else: + try: + sale_order._cart_update( + product_id=product.id, set_qty=float(qty) + ) + except UserError as e: + import_status = "warn" + failed_products.append( + f"Line {index}. {row['default_code']}: {str(e)}" + ) + + return import_status, import_msg, failed_products + + @http.route("/shop/cart", type="http", auth="public", website=True, sitemap=False) + def cart(self, access_token=None, revive="", **post): + file = post.get("cart_file") + if file: + sale_order = request.website.sale_get_order(force_create=True) + import_status, import_msg, failed_products = self.import_file( + sale_order, file + ) + post.update( + { + "import_status": import_status, + "import_msg": import_msg, + "failed_products": failed_products, + } + ) + return super().cart(access_token, revive, **post) + + def _cart_values(self, **post): + res = super()._cart_values(**post) + if "import_status" in post: + res["import_status"] = post.get("import_status") + if "import_msg" in post: + res["import_msg"] = post.get("import_msg") + if "failed_products" in post: + res["failed_products"] = post.get("failed_products") + return res + + @http.route("/shop/cart/import/example", auth="public") + def cart_import_example(self): + attachment = request.env.ref( + "website_sale_cart_add_product_xlsx_csv.cart_import_example" + ).sudo() + filecontent = base64.b64decode(attachment.datas) + filename = f"{attachment.name}.xlsx" + + return request.make_response( + filecontent, + [ + ("Content-Type", attachment.mimetype), + ("Content-Disposition", content_disposition(filename)), + ], + ) diff --git a/website_sale_cart_add_product_xlsx_csv/data/import_file_example.xml b/website_sale_cart_add_product_xlsx_csv/data/import_file_example.xml new file mode 100644 index 0000000000..3424866b18 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/data/import_file_example.xml @@ -0,0 +1,16 @@ + + + + application/vnd.ms-excel + cart_import_example + binary + + website_sale_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx + + diff --git a/website_sale_cart_add_product_xlsx_csv/i18n/es.po b/website_sale_cart_add_product_xlsx_csv/i18n/es.po new file mode 100644 index 0000000000..fadf1c9a68 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/i18n/es.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_cart_add_product_xlsx_csv +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 10:44+0000\n" +"PO-Revision-Date: 2025-10-23 12:46+0200\n" +"Last-Translator: \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: \n" +"X-Generator: Poedit 3.0.1\n" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "" +"You can import the cart " +"with a xlsx/csv file" +msgstr "" +"Puedes importar el " +"carrito con un archivo xlsx o csv" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Download example file" +msgstr "Descargar archivo de ejemplo" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Cart Import Button" +msgstr "Botón de importación de carrito" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Cart Import Button File Size Limit (MB)" +msgstr "Límite de tamaño de archivo (MB) del botón de importación de carrito" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_res_config_settings__cart_import_button_file_limit +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_website__cart_import_button_file_limit +msgid "Cart import button file size limit (MB)" +msgstr "Límite de tamaño de archivo (MB) del botón de importación de carrito" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model,name:website_sale_cart_add_product_xlsx_csv.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Enable or disable the cart import button" +msgstr "Habilitar o deshabilitar el botón de importación de carrito" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Import success" +msgstr "Importación exitosa" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Importation error." +msgstr "Error de importación." + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Incorrect file format, it must me a .xlsx or a .csv" +msgstr "Formato de archivo incorrecto, debe ser un .xlsx o un csv" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "" +"Incorrect file format, the columns should be 'default_code' and " +"'product_uom_qty'" +msgstr "" +"Formato de archivo incorrecto, las columnas deben ser 'default_code' y " +"'product_uom_qty'" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_res_config_settings__cart_import_button +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_website__cart_import_button +msgid "Is the cart import button enabled?" +msgstr "¿Está el botón de importar carrito habilitado?" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Partial import. The following products had errors:" +msgstr "Importación parcial. Los siguientes productos tienen errores:" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product can not be sold" +msgstr "El producto no puede ser vendido" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product is not published on the website" +msgstr "El producto no está publicado en el sitio web" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product max saleable qty is {}" +msgstr "La cantidad máxima que se puede vender es: {}" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The file size is greater than the maximum allowed, {} MB" +msgstr "El tamaño de archivo es más grande que el máximo permitido, {} MB" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.constraint,message:website_sale_cart_add_product_xlsx_csv.constraint_website_cart_import_button_file_limit_min +msgid "The file size limit must be positive" +msgstr "El tamaño limite debe ser positivo" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The product is archived" +msgstr "El producto está archivado" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The product was not found" +msgstr "El producto no fue encontrado" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, fuzzy, python-format +msgid "The quantity is not a number" +msgstr "La cantidad debe der un número positivo" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The quantity should be a positive number" +msgstr "La cantidad debe der un número positivo" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model,name:website_sale_cart_add_product_xlsx_csv.model_website +msgid "Website" +msgstr "Sitio web" diff --git a/website_sale_cart_add_product_xlsx_csv/i18n/website_sale_cart_add_product_xlsx_csv.pot b/website_sale_cart_add_product_xlsx_csv/i18n/website_sale_cart_add_product_xlsx_csv.pot new file mode 100644 index 0000000000..437fe206fc --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/i18n/website_sale_cart_add_product_xlsx_csv.pot @@ -0,0 +1,155 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_cart_add_product_xlsx_csv +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "" +"You can import the cart" +" with a xlsx/csv file" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Download example file" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Cart Import Button" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Cart Import Button File Size Limit (MB)" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_res_config_settings__cart_import_button_file_limit +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_website__cart_import_button_file_limit +msgid "Cart import button file size limit (MB)" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model,name:website_sale_cart_add_product_xlsx_csv.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.res_config_settings_view_form +msgid "Enable or disable the cart import button" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Import success" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Importation error." +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Incorrect file format, it must me a .xlsx or a .csv" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "" +"Incorrect file format, the columns should be 'default_code' and " +"'product_uom_qty'" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_res_config_settings__cart_import_button +#: model:ir.model.fields,field_description:website_sale_cart_add_product_xlsx_csv.field_website__cart_import_button +msgid "Is the cart import button enabled?" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model_terms:ir.ui.view,arch_db:website_sale_cart_add_product_xlsx_csv.cart_lines_import +msgid "Partial import. The following products had errors:" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product can not be sold" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product is not published on the website" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "Product max saleable qty is {}" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The file size is greater than the maximum allowed, {} MB" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model.constraint,message:website_sale_cart_add_product_xlsx_csv.constraint_website_cart_import_button_file_limit_min +msgid "The file size limit must be positive" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The product is archived" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The product was not found" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The quantity is not a number" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#. odoo-python +#: code:addons/website_sale_cart_add_product_xlsx_csv/controllers/main.py:0 +#, python-format +msgid "The quantity should be a positive number" +msgstr "" + +#. module: website_sale_cart_add_product_xlsx_csv +#: model:ir.model,name:website_sale_cart_add_product_xlsx_csv.model_website +msgid "Website" +msgstr "" diff --git a/website_sale_cart_add_product_xlsx_csv/models/__init__.py b/website_sale_cart_add_product_xlsx_csv/models/__init__.py new file mode 100644 index 0000000000..a424e440f1 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import res_config_settings +from . import website diff --git a/website_sale_cart_add_product_xlsx_csv/models/res_config_settings.py b/website_sale_cart_add_product_xlsx_csv/models/res_config_settings.py new file mode 100644 index 0000000000..350f6ddcbc --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/models/res_config_settings.py @@ -0,0 +1,20 @@ +# Copyright 2025 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + cart_import_button = fields.Boolean( + string="Is the cart import button enabled?", + related="website_id.cart_import_button", + readonly=False, + ) + + cart_import_button_file_limit = fields.Float( + string="Cart import button file size limit (MB)", + related="website_id.cart_import_button_file_limit", + readonly=False, + ) diff --git a/website_sale_cart_add_product_xlsx_csv/models/website.py b/website_sale_cart_add_product_xlsx_csv/models/website.py new file mode 100644 index 0000000000..9c7907c910 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/models/website.py @@ -0,0 +1,21 @@ +# Copyright 2025 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class Website(models.Model): + _inherit = "website" + + cart_import_button = fields.Boolean(string="Is the cart import button enabled?") + cart_import_button_file_limit = fields.Float( + string="Cart import button file size limit (MB)", default=2 + ) + + _sql_constraints = [ + ( + "cart_import_button_file_limit_min", + "CHECK (vat_prorate > 0)", + "The file size limit must be positive", + ), + ] diff --git a/website_sale_cart_add_product_xlsx_csv/pyproject.toml b/website_sale_cart_add_product_xlsx_csv/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_sale_cart_add_product_xlsx_csv/readme/CONFIGURE.md b/website_sale_cart_add_product_xlsx_csv/readme/CONFIGURE.md new file mode 100755 index 0000000000..4d151b0761 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/readme/CONFIGURE.md @@ -0,0 +1,4 @@ +To configure this module, you need to: + +- To enable the cart import button, go to Website / Configuration / Settings. Then search the "Cart Import Button" and set its value. +- To edit the maximum file size, go to Website / Configuration / Settings. Then search the "Cart import button file size limit" and edit its value. diff --git a/website_sale_cart_add_product_xlsx_csv/readme/CONTRIBUTORS.md b/website_sale_cart_add_product_xlsx_csv/readme/CONTRIBUTORS.md new file mode 100755 index 0000000000..f566245042 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Sygel](https://www.sygel.es): + - Alberto Martínez + - Valentin Vinagre + - Harald Panten diff --git a/website_sale_cart_add_product_xlsx_csv/readme/DESCRIPTION.md b/website_sale_cart_add_product_xlsx_csv/readme/DESCRIPTION.md new file mode 100755 index 0000000000..bfdf7150fb --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module adds a button in the website cart that let users import a xlsx or csv file with the products and quantities that he wants to add. + +This button is only shown on empty carts, and the file must contain the product's internal reference and the quantity to buy. diff --git a/website_sale_cart_add_product_xlsx_csv/readme/ROADMAP.md b/website_sale_cart_add_product_xlsx_csv/readme/ROADMAP.md new file mode 100644 index 0000000000..0de6f3436d --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/readme/ROADMAP.md @@ -0,0 +1 @@ +- Despithe the fact that the cart button is enabled by default, some Odoo instances have the website configured to hide it if it is empty. If the shopping cart button is hidden without anything in it, the functionality cannot be used until something is added to the cart. diff --git a/website_sale_cart_add_product_xlsx_csv/readme/USAGE.md b/website_sale_cart_add_product_xlsx_csv/readme/USAGE.md new file mode 100755 index 0000000000..b50ab614c8 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/readme/USAGE.md @@ -0,0 +1,9 @@ +To use this module, you need to: + +1. Go to the "/shop/cart" path of your server website. Example: "http://localhost:8069/shop/cart" +2. You will see your cart. Empty it if it has products. +3. Click on "Download example file". Fill the xlsx file rows: + 3.1. The "default_code" column is the internal reference of the product that you are going to add to your cart + 3.2. The "product_uom_qty" is the qty of the product that you are going to add to your cart +4. Select the filled file, and click the "Import" button +5. You will see the cart with the imported products. If some error hapened or there are products that could not be imported, the system will show a message diff --git a/website_sale_cart_add_product_xlsx_csv/static/description/icon.png b/website_sale_cart_add_product_xlsx_csv/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/website_sale_cart_add_product_xlsx_csv/static/description/icon.png differ diff --git a/website_sale_cart_add_product_xlsx_csv/static/description/icon.svg b/website_sale_cart_add_product_xlsx_csv/static/description/icon.svg new file mode 100644 index 0000000000..a7a26d0932 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/website_sale_cart_add_product_xlsx_csv/static/description/index.html b/website_sale_cart_add_product_xlsx_csv/static/description/index.html new file mode 100644 index 0000000000..f26610ac87 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Website Sale Cart Add Product Xlsx Csv

+ +

Beta License: AGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runboat

+

This module adds a button in the website cart that let users import a +xlsx or csv file with the products and quantities that he wants to add.

+

This button is only shown on empty carts, and the file must contain the +product’s internal reference and the quantity to buy.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  • To enable the cart import button, go to Website / Configuration / +Settings. Then search the “Cart Import Button” and set its value.
  • +
  • To edit the maximum file size, go to Website / Configuration / +Settings. Then search the “Cart import button file size limit” and +edit its value.
  • +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to the “/shop/cart” path of your server website. Example: +“http://localhost:8069/shop/cart
  2. +
  3. You will see your cart. Empty it if it has products.
  4. +
  5. Click on “Download example file”. Fill the xlsx file rows: 3.1. The +“default_code” column is the internal reference of the product that +you are going to add to your cart 3.2. The “product_uom_qty” is the +qty of the product that you are going to add to your cart
  6. +
  7. Select the filled file, and click the “Import” button
  8. +
  9. You will see the cart with the imported products. If some error +hapened or there are products that could not be imported, the system +will show a message
  10. +
+
+
+

Known issues / Roadmap

+
    +
  • Despithe the fact that the cart button is enabled by default, some +Odoo instances have the website configured to hide it if it is empty. +If the shopping cart button is hidden without anything in it, the +functionality cannot be used until something is added to the cart.
  • +
+
+
+

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

+
    +
  • Sygel
  • +
+
+
+

Contributors

+
    +
  • Sygel:
      +
    • Alberto Martínez
    • +
    • Valentin Vinagre
    • +
    • Harald Panten
    • +
    +
  • +
+
+
+

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.

+

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_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx new file mode 100644 index 0000000000..5bb8476702 Binary files /dev/null and b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx differ diff --git a/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.csv b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.csv new file mode 100644 index 0000000000..9627408dca --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.csv @@ -0,0 +1,7 @@ +default_code,product_uom_qty +E-COM12,2 +FURN_0097,1 +FURN_8900,1 +FURN_7800,-1 +FURN_9001,1 +Inexistent,1 diff --git a/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.error b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.error new file mode 100644 index 0000000000..9627408dca --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.error @@ -0,0 +1,7 @@ +default_code,product_uom_qty +E-COM12,2 +FURN_0097,1 +FURN_8900,1 +FURN_7800,-1 +FURN_9001,1 +Inexistent,1 diff --git a/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.xlsx b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.xlsx new file mode 100644 index 0000000000..58d358fa54 Binary files /dev/null and b/website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.xlsx differ diff --git a/website_sale_cart_add_product_xlsx_csv/tests/__init__.py b/website_sale_cart_add_product_xlsx_csv/tests/__init__.py new file mode 100644 index 0000000000..7dc199512d --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_website_sale_cart_add_product_xlsx_csv diff --git a/website_sale_cart_add_product_xlsx_csv/tests/test_website_sale_cart_add_product_xlsx_csv.py b/website_sale_cart_add_product_xlsx_csv/tests/test_website_sale_cart_add_product_xlsx_csv.py new file mode 100644 index 0000000000..d26200d381 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/tests/test_website_sale_cart_add_product_xlsx_csv.py @@ -0,0 +1,95 @@ +# Copyright 2025 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from io import BytesIO + +from werkzeug.datastructures import FileStorage + +from odoo.tests.common import tagged +from odoo.tools.misc import file_path + +from odoo.addons.website.tools import MockRequest +from odoo.addons.website_sale.tests.test_website_sale_cart import WebsiteSaleCart +from odoo.addons.website_sale_cart_add_product_xlsx_csv.controllers.main import ( + WebsiteSaleAddProductXlsxCsv, +) + + +@tagged("post_install", "-at_install") +class TestWebsiteSaleCartAddProductXlsxCsv(WebsiteSaleCart): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.WebsiteSaleControllerAddProductXlsxCsv = WebsiteSaleAddProductXlsxCsv() + cls.file_route_csv = file_path( + "website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.csv" + ) + cls.file_route_xlsx = file_path( + "website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.xlsx" + ) + cls.error_file_route_csv = file_path( + "website_sale_cart_add_product_xlsx_csv/static/xlsx/import_unit_test.error" + ) + + def read_file(self, path): + with open(path, "rb") as f: + file = FileStorage(stream=BytesIO(f.read()), filename=path.split("/")[-1]) + return file + + def _test_import(self, file_route): + website = self.website.with_user(self.public_user) + with MockRequest(self.product.with_user(self.public_user).env, website=website): + sale_order = website.sale_get_order(force_create=True) + self.assertFalse(sale_order.order_line) + self.WebsiteSaleControllerAddProductXlsxCsv.cart( + cart_file=self.read_file(file_route) + ) + self.assertTrue(sale_order.order_line) + + def test_import_csv(self): + self._test_import(self.file_route_csv) + + def test_import_xlsx(self): + self._test_import(self.file_route_xlsx) + + def test_import_vals(self): + website = self.website.with_user(self.public_user) + with MockRequest(self.product.with_user(self.public_user).env, website=website): + sale_order = website.sale_get_order(force_create=True) + self.assertFalse(sale_order.order_line) + ( + import_status, + _, + failed_products, + ) = self.WebsiteSaleControllerAddProductXlsxCsv.import_file( + sale_order, self.read_file(self.file_route_csv) + ) + self.assertEqual(import_status, "warn") + product_to_fail = ["FURN_7800", "FURN_9001", "Inexistent"] + failed_products = "\n".join(failed_products) + for p in product_to_fail: + self.assertIn(p, failed_products) + + def test_import_error(self): + website = self.website.with_user(self.public_user) + with MockRequest(self.product.with_user(self.public_user).env, website=website): + sale_order = website.sale_get_order(force_create=True) + self.assertFalse(sale_order.order_line) + self.WebsiteSaleControllerAddProductXlsxCsv.cart( + cart_file=self.read_file(self.error_file_route_csv) + ) + self.assertFalse(sale_order.order_line) + + def test_import_error_vals(self): + website = self.website.with_user(self.public_user) + with MockRequest(self.product.with_user(self.public_user).env, website=website): + sale_order = website.sale_get_order(force_create=True) + self.assertFalse(sale_order.order_line) + ( + import_status, + _, + _, + ) = self.WebsiteSaleControllerAddProductXlsxCsv.import_file( + sale_order, self.read_file(self.error_file_route_csv) + ) + self.assertEqual(import_status, "error") diff --git a/website_sale_cart_add_product_xlsx_csv/views/cart_templates.xml b/website_sale_cart_add_product_xlsx_csv/views/cart_templates.xml new file mode 100644 index 0000000000..ecd08714e4 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/views/cart_templates.xml @@ -0,0 +1,89 @@ + + + + + + + diff --git a/website_sale_cart_add_product_xlsx_csv/views/res_config_settings.xml b/website_sale_cart_add_product_xlsx_csv/views/res_config_settings.xml new file mode 100644 index 0000000000..58290d87a4 --- /dev/null +++ b/website_sale_cart_add_product_xlsx_csv/views/res_config_settings.xml @@ -0,0 +1,28 @@ + + + + + res.config.settings.view.form.inherit.website.sale + res.config.settings + + + + + + + + + + + + +