Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <a href='https://github.com/Yajo'><img src='https://github.com/Yajo.png' width='32' height='32' style='border-radius:50%;' alt='Yajo'/></a> | 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
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# generated from manifests external_dependencies
openpyxl
3 changes: 2 additions & 1 deletion setup/_metapackage/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
124 changes: 124 additions & 0 deletions website_sale_cart_add_product_xlsx_csv/README.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/OCA/e-commerce/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 <https://github.com/OCA/e-commerce/issues/new?body=module:%20website_sale_cart_add_product_xlsx_csv%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Sygel

Contributors
------------

- `Sygel <https://www.sygel.es>`__:

- 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 <https://github.com/OCA/e-commerce/tree/17.0/website_sale_cart_add_product_xlsx_csv>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions website_sale_cart_add_product_xlsx_csv/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import controllers
from . import models
21 changes: 21 additions & 0 deletions website_sale_cart_add_product_xlsx_csv/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2025 Alberto Martínez <alberto.martinez@sygel.es>
# 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",
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import main
178 changes: 178 additions & 0 deletions website_sale_cart_add_product_xlsx_csv/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Copyright 2025 Alberto Martínez <alberto.martinez@sygel.es>
# 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)),
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="cart_import_example" model="ir.attachment">
<field name="mimetype">application/vnd.ms-excel</field>
<field name="name">cart_import_example</field>
<field name="type">binary</field>
<field
name="datas"
type="base64"
file="website_sale_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx"
/>
<field
name="url"
>website_sale_cart_add_product_xlsx_csv/static/xlsx/import_example.xlsx</field>
</record>
</odoo>
Loading
Loading