From e08b9124984a324e2333620daf1d7439da2cc95c Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Tue, 20 Jun 2023 14:43:37 +0200 Subject: [PATCH 01/11] [ADD] connector_extension_woocommerce --- connector_extension_woocommerce/README.rst | 64 +++ connector_extension_woocommerce/__init__.py | 1 + .../__manifest__.py | 15 + .../components/__init__.py | 1 + .../components/adapter.py | 212 +++++++++ connector_extension_woocommerce/i18n/ca.po | 82 ++++ connector_extension_woocommerce/i18n/es.po | 82 ++++ .../readme/CONTRIBUTORS.rst | 4 + .../readme/DESCRIPTION.rst | 1 + .../static/description/icon.png | Bin 0 -> 6342 bytes .../static/description/index.html | 421 ++++++++++++++++++ .../addons/connector_extension_woocommerce | 1 + .../connector_extension_woocommerce/setup.py | 6 + 13 files changed, 890 insertions(+) create mode 100644 connector_extension_woocommerce/README.rst create mode 100644 connector_extension_woocommerce/__init__.py create mode 100644 connector_extension_woocommerce/__manifest__.py create mode 100644 connector_extension_woocommerce/components/__init__.py create mode 100644 connector_extension_woocommerce/components/adapter.py create mode 100644 connector_extension_woocommerce/i18n/ca.po create mode 100644 connector_extension_woocommerce/i18n/es.po create mode 100644 connector_extension_woocommerce/readme/CONTRIBUTORS.rst create mode 100644 connector_extension_woocommerce/readme/DESCRIPTION.rst create mode 100644 connector_extension_woocommerce/static/description/icon.png create mode 100644 connector_extension_woocommerce/static/description/index.html create mode 120000 setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce create mode 100644 setup/connector_extension_woocommerce/setup.py diff --git a/connector_extension_woocommerce/README.rst b/connector_extension_woocommerce/README.rst new file mode 100644 index 000000000..4059706b0 --- /dev/null +++ b/connector_extension_woocommerce/README.rst @@ -0,0 +1,64 @@ +=============================== +Connector Extension Woocommerce +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f381547277a60fe3768f6861b4c213c845b21507225ce930457b473de3c08289 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/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-NuoBiT%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/NuoBiT/odoo-addons/tree/14.0/connector_extension_woocommerce + :alt: NuoBiT/odoo-addons + +|badge1| |badge2| |badge3| + +This module extends the connector extension module to add support for Woocommerce + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* NuoBiT Solutions +* SL + +Contributors +~~~~~~~~~~~~ + +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones + +Maintainers +~~~~~~~~~~~ + +This module is part of the `NuoBiT/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/connector_extension_woocommerce/__init__.py b/connector_extension_woocommerce/__init__.py new file mode 100644 index 000000000..1377f57f5 --- /dev/null +++ b/connector_extension_woocommerce/__init__.py @@ -0,0 +1 @@ +from . import components diff --git a/connector_extension_woocommerce/__manifest__.py b/connector_extension_woocommerce/__manifest__.py new file mode 100644 index 000000000..1167367b1 --- /dev/null +++ b/connector_extension_woocommerce/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright NuoBiT Solutions - Kilian Niubo +# Copyright NuoBiT Solutions - Eric Antones +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Connector Extension Woocommerce", + "summary": "This module extends the connector extension module " + "to add support for Woocommerce", + "version": "14.0.1.0.0", + "author": "NuoBiT Solutions, SL", + "license": "LGPL-3", + "category": "Connector", + "website": "https://github.com/nuobit/odoo-addons", + "depends": ["connector_extension"], +} diff --git a/connector_extension_woocommerce/components/__init__.py b/connector_extension_woocommerce/components/__init__.py new file mode 100644 index 000000000..f502287fe --- /dev/null +++ b/connector_extension_woocommerce/components/__init__.py @@ -0,0 +1 @@ +from . import adapter diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py new file mode 100644 index 000000000..f7a2b2e36 --- /dev/null +++ b/connector_extension_woocommerce/components/adapter.py @@ -0,0 +1,212 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Kilian Niubo +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +import json +import logging + +from requests.exceptions import ConnectionError as RequestConnectionError + +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import RetryableJobError + +from ...connector_extension.common.tools import trim_domain + +_logger = logging.getLogger(__name__) + + +class ConnectorExtensionWooCommerceAdapterCRUD(AbstractComponent): + _name = "connector.extension.woocommerce.adapter.crud" + _inherit = "connector.extension.adapter.crud" + + def _exec(self, op, resource, *args, **kwargs): + if kwargs.get("domain"): + kwargs["domain"] = trim_domain(kwargs["domain"]) + func = getattr(self, "_exec_%s" % op) + return func(resource, *args, **kwargs) + + def _manage_error_codes( + self, res_data, res, resource, raise_on_error=True, **kwargs + ): + if not res.ok: + error_message = None + if res.status_code == 404: + if res_data.get("code") == "rest_no_route": + error_message = _( + "Error: '%s'. Probably the %s has been" + " removed from Woocommerce. " + "If it's the case, try to remove the binding of the %s." + % (res_data.get("message"), resource, self.model._name) + ) + elif res_data.get("code") == "woocommerce_rest_term_invalid": + error_message = _( + "Error: '%s'. Probably the %s has been " + "removed from Woocommerce. " + "If it's the case, try to remove the binding of the %s." + % (res_data.get("message"), resource, self.model._name) + ) + elif ( + res_data.get("code") + == "woocommerce_rest_product_variation_invalid_parent" + ): + error_message = _( + "Error: '%s'. Probably the product in %s " + "has been removed from Woocommerce. " + "If it's the case, try to remove the binding of the " + "woocommerce.product.template" + % ( + res_data.get("message"), + resource, + ) + ) + elif res.status_code == 400: + if res_data.get("code") == "term_exists": + error_message = _( + "Error: '%s'. Probably repeated record already exists in Woocommerce.\n" + "Please, review the data in %s/%s and compare it with %s" + % ( + res_data["message"], + resource, + res_data["res_data"]["resource_id"], + kwargs["data"], + ) + ) + elif res_data.get("code") in [ + "woocommerce_rest_product_variation_invalid_id", + "woocommerce_rest_product_invalid_id", + ]: + error_message = _( + "Error: '%s'. Probably the %s has been removed from Woocommerce. " + "If it's the case, try to remove the binding of the %s." + % (res_data.get("message"), resource, self.model._name) + ) + if not error_message: + error_message = _("Error: %s, Resource: %s" % (res_data, resource)) + if raise_on_error: + raise ValidationError(error_message) + return error_message + return res_data + + # TODO: remove this total items and use the res.headers instead + def _get_res_total_items(self, res): + headers = res.headers + total_items = headers.get("X-WP-Total") or 0 + if total_items: + total_items = int(headers.get("X-WP-Total")) + return total_items + + def _exec_wcapi_call(self, op, resource, *args, **kwargs): + func = getattr(self.wcapi, op) + try: + res = func(resource, *args, **kwargs) + res_data = res.json() + if "data" in res_data: + res_data["res_data"] = res_data.pop("data") + res_data = self._manage_error_codes(res_data, res, resource, **kwargs) + total_items = self._get_res_total_items(res) + result = { + "ok": res.ok, + "status_code": res.status_code, + "total_items": total_items, + "data": res_data, + } + except RequestConnectionError as e: + raise RetryableJobError(_("Error connecting to WooCommerce: %s") % e) from e + except json.decoder.JSONDecodeError as e: + raise ValidationError( + _( + "Error decoding json WooCommerce response: " + "%s\nArgs:%s\nKwargs:%s\n" + "URL:%s\nHeaders:%s\nMethod:%s\nBody:%s" + ) + % ( + e, + args, + kwargs, + res.url, + res.request.headers, + res.request.method, + res.text and res.text[:100] + " ...", + ) + ) from e + return result + + def get_total_items(self, resource, domain=None): + filters_values = self._get_search_fields() + real_domain, common_domain = self._extract_domain_clauses( + domain, filters_values + ) + params = self._domain_to_normalized_dict(real_domain) + params["per_page"] = 1 + result = self._exec_wcapi_call("get", resource, params=params) + return result["total_items"] + + def _get_search_fields(self): + return ["modified_after", "offset", "per_page", "page"] + + def _exec_get(self, resource, *args, **kwargs): + if resource == "system_status": + return self._exec_wcapi_call("get", resource, *args, **kwargs) + # WooCommerce has the parameter next on the response headers + # to get the next page but we can't use it because if we use + # the offset, the next page will have the same items as the first page. + # It looks like a bug in WooCommerce API. + domain = [] + if "domain" in kwargs: + domain = kwargs.pop("domain") + search_fields = self._get_search_fields() + real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) + params = self._domain_to_normalized_dict(real_domain) + if "limit" in kwargs: + limit = kwargs.pop("limit") + else: + limit = self.get_total_items(resource, domain) + params["offset"] = ( + kwargs.pop("offset") if "offset" in kwargs and "offset" not in params else 0 + ) + page_size = self.backend_record.page_size + params["per_page"] = page_size if page_size > 0 else 100 + data = [] + while len(data) < limit: + if page_size > limit - len(data): + params["per_page"] = limit - len(data) + res = self._exec_wcapi_call("get", resource, params=params, *args, **kwargs) + # WooCommerce returns a dict if the response is a single item + if not isinstance(res["data"], list): + res["data"] = [res["data"]] + data += res["data"] + params["offset"] += len(res["data"]) + return self._filter(data, common_domain) + + def _exec_post(self, resource, *args, **kwargs): + res = self._exec_wcapi_call( + "post", + resource, + *args, + **kwargs, + ) + return res["data"] + + def _exec_put(self, resource, *args, **kwargs): + return self._exec_wcapi_call("put", resource, *args, **kwargs) + + def _exec_delete(self, resource, *args, **kwargs): + return self._exec_wcapi_call( + "delete", + resource, + *args, + **kwargs, + ) + + def _exec_options(self, resource, *args, **kwargs): + raise NotImplementedError() + + def get_version(self): + system_status = self._exec("get", "system_status") + version = False + if system_status: + version = system_status["data"].get("environment").get("version") + return version diff --git a/connector_extension_woocommerce/i18n/ca.po b/connector_extension_woocommerce/i18n/ca.po new file mode 100644 index 000000000..9a5aa5098 --- /dev/null +++ b/connector_extension_woocommerce/i18n/ca.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * connector_extension_woocommerce +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-22 12:51+0000\n" +"PO-Revision-Date: 2024-10-22 12:51+0000\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: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "Error connecting to WooCommerce: %s" +msgstr "Error al connectar amb WooCommerce: %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error decoding json WooCommerce response: %s\n" +"Args:%s\n" +"Kwargs:%s\n" +"URL:%s\n" +"Headers:%s\n" +"Method:%s\n" +"Body:%s" +msgstr "" +"Error decodificant la resposta json de WooCommerce: %s\n" +"Args:%s\n" +"Kwargs:%s\n" +"URL:%s\n" +"Headers:%s\n" +"Method:%s\n" +"Body:%s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "Error: %s, Resource: %s" +msgstr "Error: %s, Recurs: %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably repeated record already exists in Woocommerce.\n" +"Please, review the data in %s/%s and compare it with %s" +msgstr "" +"Error: '%s'. Probablement el registre repetit ja existeix a Woocommerce.\n" +"Si us plau, revisa les dades a %s/%s i compara-les amb %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably the %s has been removed from Woocommerce. If it's the " +"case, try to remove the binding of the %s." +msgstr "" +"Error: '%s'. Probablement el %s ha estat eliminat de Woocommerce. Si és el " +"cas, intenta eliminar la vinculació del %s." + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably the product in %s has been removed from Woocommerce. " +"If it's the case, try to remove the binding of the " +"woocommerce.product.template" +msgstr "" +"Error: '%s'. Probablement el producte a %s ha estat eliminat de Woocommerce. " +"Si és el cas, intenta eliminar la vinculació del " +"woocommerce.product.template" diff --git a/connector_extension_woocommerce/i18n/es.po b/connector_extension_woocommerce/i18n/es.po new file mode 100644 index 000000000..2fe5efed6 --- /dev/null +++ b/connector_extension_woocommerce/i18n/es.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * connector_extension_woocommerce +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-22 12:50+0000\n" +"PO-Revision-Date: 2024-10-22 12:50+0000\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: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "Error connecting to WooCommerce: %s" +msgstr "Error de conexión a WooCommerce: %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error decoding json WooCommerce response: %s\n" +"Args:%s\n" +"Kwargs:%s\n" +"URL:%s\n" +"Headers:%s\n" +"Method:%s\n" +"Body:%s" +msgstr "" +"Error al decodificar la respuesta json de WooCommerce: %s\n" +"Args:%s\n" +"Kwargs:%s\n" +"URL:%s\n" +"Headers:%s\n" +"Método:%s\n" +"Cuerpo:%s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "Error: %s, Resource: %s" +msgstr "Error: %s, Recurso: %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably repeated record already exists in Woocommerce.\n" +"Please, review the data in %s/%s and compare it with %s" +msgstr "" +"Error: '%s'. Probablemente el registro repetido ya existe en Woocommerce.\n" +"Por favor, revise los datos en %s/%s y compárelos con %s" + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably the %s has been removed from Woocommerce. If it's the " +"case, try to remove the binding of the %s." +msgstr "" +"Error: '%s'. Probablemente el %s ha sido eliminado de Woocommerce. Si es el " +"caso, intente eliminar el enlace del %s." + +#. module: connector_extension_woocommerce +#: code:addons/connector_extension_woocommerce/components/adapter.py:0 +#, python-format +msgid "" +"Error: '%s'. Probably the product in %s has been removed from Woocommerce. " +"If it's the case, try to remove the binding of the " +"woocommerce.product.template" +msgstr "" +"Error: '%s'. Probablemente el producto en %s ha sido eliminado de " +"Woocommerce. Si es el caso, intente eliminar el enlace de la " +"woocommerce.product.template" diff --git a/connector_extension_woocommerce/readme/CONTRIBUTORS.rst b/connector_extension_woocommerce/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..e468d95a0 --- /dev/null +++ b/connector_extension_woocommerce/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `NuoBiT `__: + + * Kilian Niubo + * Eric Antones diff --git a/connector_extension_woocommerce/readme/DESCRIPTION.rst b/connector_extension_woocommerce/readme/DESCRIPTION.rst new file mode 100644 index 000000000..cf1445fe3 --- /dev/null +++ b/connector_extension_woocommerce/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module extends the connector extension module to add support for Woocommerce diff --git a/connector_extension_woocommerce/static/description/icon.png b/connector_extension_woocommerce/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd641e792c30455187ca30940bc0f329ce8bbb0 GIT binary patch literal 6342 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 literal 0 HcmV?d00001 diff --git a/connector_extension_woocommerce/static/description/index.html b/connector_extension_woocommerce/static/description/index.html new file mode 100644 index 000000000..b059823a1 --- /dev/null +++ b/connector_extension_woocommerce/static/description/index.html @@ -0,0 +1,421 @@ + + + + + + +Connector Extension Woocommerce + + + +
+

Connector Extension Woocommerce

+ + +

Beta License: LGPL-3 NuoBiT/odoo-addons

+

This module extends the connector extension module to add support for Woocommerce

+

Table of contents

+ +
+

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

+
    +
  • NuoBiT Solutions
  • +
  • SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the NuoBiT/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce b/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce new file mode 120000 index 000000000..257fd24c0 --- /dev/null +++ b/setup/connector_extension_woocommerce/odoo/addons/connector_extension_woocommerce @@ -0,0 +1 @@ +../../../../connector_extension_woocommerce \ No newline at end of file diff --git a/setup/connector_extension_woocommerce/setup.py b/setup/connector_extension_woocommerce/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/connector_extension_woocommerce/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From d939dca29a4fd52a64a9071d3e3e81283405a83f Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Tue, 2 Dec 2025 13:22:14 +0100 Subject: [PATCH 02/11] [IMP+FIX+REF] connector_extension_woocommerce --- .../components/adapter.py | 240 +++++++++++++----- 1 file changed, 171 insertions(+), 69 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index f7a2b2e36..451d02db8 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -29,7 +29,7 @@ def _exec(self, op, resource, *args, **kwargs): return func(resource, *args, **kwargs) def _manage_error_codes( - self, res_data, res, resource, raise_on_error=True, **kwargs + self, op, res_data, res, resource, *args, raise_on_error=True, **kwargs ): if not res.ok: error_message = None @@ -41,27 +41,27 @@ def _manage_error_codes( "If it's the case, try to remove the binding of the %s." % (res_data.get("message"), resource, self.model._name) ) - elif res_data.get("code") == "woocommerce_rest_term_invalid": - error_message = _( - "Error: '%s'. Probably the %s has been " - "removed from Woocommerce. " - "If it's the case, try to remove the binding of the %s." - % (res_data.get("message"), resource, self.model._name) - ) - elif ( - res_data.get("code") - == "woocommerce_rest_product_variation_invalid_parent" - ): - error_message = _( - "Error: '%s'. Probably the product in %s " - "has been removed from Woocommerce. " - "If it's the case, try to remove the binding of the " - "woocommerce.product.template" - % ( - res_data.get("message"), - resource, - ) - ) + # elif res_data.get("code") == "woocommerce_rest_term_invalid": + # error_message = _( + # "Error: '%s'. Probably the %s has been " + # "removed from Woocommerce. " + # "If it's the case, try to remove the binding of the %s." + # % (res_data.get("message"), resource, self.model._name) + # ) + # elif ( + # res_data.get("code") + # == "woocommerce_rest_product_variation_invalid_parent" + # ): + # error_message = _( + # "Error: '%s'. Probably the product in %s " + # "has been removed from Woocommerce. " + # "If it's the case, try to remove the binding of the " + # "woocommerce.product.template" + # % ( + # res_data.get("message"), + # resource, + # ) + # ) elif res.status_code == 400: if res_data.get("code") == "term_exists": error_message = _( @@ -70,21 +70,24 @@ def _manage_error_codes( % ( res_data["message"], resource, - res_data["res_data"]["resource_id"], + res_data["data"]["resource_id"], kwargs["data"], ) ) - elif res_data.get("code") in [ - "woocommerce_rest_product_variation_invalid_id", - "woocommerce_rest_product_invalid_id", - ]: - error_message = _( - "Error: '%s'. Probably the %s has been removed from Woocommerce. " - "If it's the case, try to remove the binding of the %s." - % (res_data.get("message"), resource, self.model._name) - ) + # elif res_data.get("code") in [ + # "woocommerce_rest_product_variation_invalid_id", + # "woocommerce_rest_product_invalid_id", + # ]: + # error_message = _( + # "Error: '%s'. Probably the %s has been removed from Woocommerce. " + # "If it's the case, try to remove the binding of the %s." + # % (res_data.get("message"), resource, self.model._name) + # ) if not error_message: - error_message = _("Error: %s, Resource: %s" % (res_data, resource)) + error_message = _( + "Error: %s -> Op: %s, Resource: %s, Args: %s, KWArgs: %s" + % (res_data, op, resource, args, kwargs) + ) if raise_on_error: raise ValidationError(error_message) return error_message @@ -98,21 +101,92 @@ def _get_res_total_items(self, res): total_items = int(headers.get("X-WP-Total")) return total_items - def _exec_wcapi_call(self, op, resource, *args, **kwargs): + def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 func = getattr(self.wcapi, op) try: - res = func(resource, *args, **kwargs) - res_data = res.json() - if "data" in res_data: - res_data["res_data"] = res_data.pop("data") - res_data = self._manage_error_codes(res_data, res, resource, **kwargs) - total_items = self._get_res_total_items(res) + response = func(resource, *args, **kwargs) + res_data = response.json() result = { - "ok": res.ok, - "status_code": res.status_code, - "total_items": total_items, - "data": res_data, + "data": None, + "total_items": 0, + "total_pages": 0, + "next": None, + "prev": None, } + if not response.ok: + # These are the cases where a not found should be an empty result + # instead of an error, we need to bypass the standard REST behaviour + # and make it look like more like an SQL whre if the parameters are wrong + # it just returns no value + if op == "get": + if response.status_code != 404 or res_data["code"] not in ( + "woocommerce_rest_product_invalid_id", + "woocommerce_rest_product_variation_invalid_id", + "woocommerce_rest_product_variation_invalid_parent", + "woocommerce_rest_term_invalid", + ): + self._manage_error_codes( + op, res_data, response, resource, *args, **kwargs + ) + result["data"] = [] + else: + self._manage_error_codes( + op, res_data, response, resource, *args, **kwargs + ) + result["data"] = {} + else: + # check if the response is a singleton or a list + if isinstance(res_data, dict): + singleton = True + elif isinstance(res_data, list): + singleton = False + else: + raise ValidationError( + _("Unexpected response from WooCommerce: %s") % res_data + ) + # check consistency between headers and response type + multi_headers = ["X-WP-Total", "X-WP-TotalPages"] + if singleton: + for header in multi_headers: + if header in response.headers: + raise ValidationError( + _( + "The '%s' header should not be present in " + "singleton responses: %s" + ) + % (header, res_data) + ) + for link in response.links.keys(): + if link in ["next", "prev", "first", "last"]: + raise ValidationError( + _( + "The '%s' link should not be present in singleton " + "responses: %s" + ) + % (link, res_data) + ) + else: + for header in multi_headers: + if header not in response.headers: + raise ValidationError( + _("The '%s' header is missing in multi responses: %s") + % (header, res_data) + ) + if singleton: + if op == "get": + result["data"] = [res_data] + else: + result["data"] = res_data + result["total_items"] = 1 + result["total_pages"] = 1 + else: + result["data"] = res_data + result["total_items"] = int(response.headers["X-WP-Total"]) + result["total_pages"] = int(response.headers["X-WP-TotalPages"]) + if "next" in response.links: + result["next"] = response.links["next"]["url"] + if "prev" in response.links: + result["prev"] = response.links["prev"]["url"] except RequestConnectionError as e: raise RetryableJobError(_("Error connecting to WooCommerce: %s") % e) from e except json.decoder.JSONDecodeError as e: @@ -126,10 +200,10 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): e, args, kwargs, - res.url, - res.request.headers, - res.request.method, - res.text and res.text[:100] + " ...", + response.url, + response.request.headers, + response.request.method, + response.text and response.text[:100] + " ...", ) ) from e return result @@ -157,29 +231,57 @@ def _exec_get(self, resource, *args, **kwargs): domain = [] if "domain" in kwargs: domain = kwargs.pop("domain") + search_fields = self._get_search_fields() real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) - params = self._domain_to_normalized_dict(real_domain) - if "limit" in kwargs: - limit = kwargs.pop("limit") - else: + + limit = kwargs.pop("limit", None) + if limit is None: limit = self.get_total_items(resource, domain) - params["offset"] = ( - kwargs.pop("offset") if "offset" in kwargs and "offset" not in params else 0 - ) - page_size = self.backend_record.page_size - params["per_page"] = page_size if page_size > 0 else 100 - data = [] - while len(data) < limit: - if page_size > limit - len(data): - params["per_page"] = limit - len(data) - res = self._exec_wcapi_call("get", resource, params=params, *args, **kwargs) - # WooCommerce returns a dict if the response is a single item - if not isinstance(res["data"], list): - res["data"] = [res["data"]] - data += res["data"] - params["offset"] += len(res["data"]) - return self._filter(data, common_domain) + + all_data = [] + if limit > 0: + params = self._domain_to_normalized_dict(real_domain) + offset = kwargs.pop("offset", 0) or 0 + params["offset"] = offset if offset >= 0 else 0 + page_size = self.backend_record.page_size + params["per_page"] = limit if page_size < 0 else page_size + count = 0 + end = False + while not end: + if limit is not None: + if count + page_size > limit: + diff = limit - count + if diff <= 0: + raise ValidationError( + _( + "Unexpected error in pagination diff: %s, count: %s, " + "page_size: %s, limit: %s, params: %s" + ) + % ( + diff, + count, + page_size, + limit, + params, + ) + ) + params["per_page"] = diff + end = True + res = self._exec_wcapi_call( + "get", resource, params=params, *args, **kwargs + ) + all_data += res["data"] + if params["per_page"] != len(res["data"]): + raise ValidationError( + _( + "Unexpected error in pagination. The number of items " + "retrieved is different than the number aked." + ) + ) + count += len(res["data"]) + params["offset"] += len(res["data"]) + return self._filter(all_data, common_domain) def _exec_post(self, resource, *args, **kwargs): res = self._exec_wcapi_call( @@ -208,5 +310,5 @@ def get_version(self): system_status = self._exec("get", "system_status") version = False if system_status: - version = system_status["data"].get("environment").get("version") + version = system_status["data"].get("environment", {}).get("version") return version From cecf5c831f7449916b521002c9f1fabf941aba83 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 24 Dec 2025 18:31:41 +0100 Subject: [PATCH 03/11] [IMP] connector_extension_woocommerce: new wcapi exec_get logic unified and optimized --- .../components/adapter.py | 291 +++++++++++++----- 1 file changed, 209 insertions(+), 82 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index 451d02db8..001c05293 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -93,25 +93,46 @@ def _manage_error_codes( return error_message return res_data - # TODO: remove this total items and use the res.headers instead - def _get_res_total_items(self, res): - headers = res.headers - total_items = headers.get("X-WP-Total") or 0 - if total_items: - total_items = int(headers.get("X-WP-Total")) - return total_items + # # TODO: remove this total items and use the res.headers instead + # def _get_res_total_items(self, res): + # headers = res.headers + # total_items = headers.get("X-WP-Total") or 0 + # if total_items: + # total_items = int(headers.get("X-WP-Total")) + # return total_items + # TODO: Remove *args and *kwargs and put params=None + # Check other methods than get to see if it'll work for them too def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 + if self.backend_record.enable_call_logging: + _logger.info( + "WooCommerce API Call - OP: %s, Resource: %s, Args: %s, KWArgs: %s", + op, + resource, + args, + kwargs, + ) + + params = kwargs.get("params", {}) + if {"page", "offset"}.issubset(params): + raise ValidationError( + _( + "The 'offset' and 'page' parameters are incompatible " + "in WooCommerce API calls. Please, use only one of them." + ) + ) + func = getattr(self.wcapi, op) try: response = func(resource, *args, **kwargs) res_data = response.json() result = { "data": None, + "returned_items": 0, "total_items": 0, "total_pages": 0, - "next": None, - "prev": None, + # "next": None, + # "prev": None, } if not response.ok: # These are the cases where a not found should be an empty result @@ -156,15 +177,15 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 ) % (header, res_data) ) - for link in response.links.keys(): - if link in ["next", "prev", "first", "last"]: - raise ValidationError( - _( - "The '%s' link should not be present in singleton " - "responses: %s" - ) - % (link, res_data) - ) + # for link in response.links.keys(): + # if link in ["next", "prev", "first", "last"]: + # raise ValidationError( + # _( + # "The '%s' link should not be present in singleton " + # "responses: %s" + # ) + # % (link, res_data) + # ) else: for header in multi_headers: if header not in response.headers: @@ -180,13 +201,20 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 result["total_items"] = 1 result["total_pages"] = 1 else: - result["data"] = res_data - result["total_items"] = int(response.headers["X-WP-Total"]) - result["total_pages"] = int(response.headers["X-WP-TotalPages"]) - if "next" in response.links: - result["next"] = response.links["next"]["url"] - if "prev" in response.links: - result["prev"] = response.links["prev"]["url"] + if op == "get": + result["data"] = res_data + result["total_items"] = int(response.headers["X-WP-Total"]) + result["total_pages"] = int(response.headers["X-WP-TotalPages"]) + # if "next" in response.links: + # result["next"] = response.links["next"]["url"] + # if "prev" in response.links: + # result["prev"] = response.links["prev"]["url"] + else: + raise ValidationError( + _("Unexpected multi-response for operation '%s': %s") + % (op, res_data) + ) + result["returned_items"] = len(result["data"]) except RequestConnectionError as e: raise RetryableJobError(_("Error connecting to WooCommerce: %s") % e) from e except json.decoder.JSONDecodeError as e: @@ -206,82 +234,181 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 response.text and response.text[:100] + " ...", ) ) from e + + if self.backend_record.enable_call_logging: + result_debug = dict(result) + result_debug.pop("data") + _logger.info("WooCommerce API Response: %s", result_debug) return result - def get_total_items(self, resource, domain=None): - filters_values = self._get_search_fields() - real_domain, common_domain = self._extract_domain_clauses( - domain, filters_values - ) - params = self._domain_to_normalized_dict(real_domain) - params["per_page"] = 1 - result = self._exec_wcapi_call("get", resource, params=params) - return result["total_items"] + # def get_total_items(self, resource, domain=None): + # filters_values = self._get_search_fields() + # real_domain, common_domain = self._extract_domain_clauses( + # domain, filters_values + # ) + # params = self._domain_to_normalized_dict(real_domain) + # if not common_domain: + # params["per_page"] = 1 + # result = self._exec_wcapi_call("get", resource, params=params) + # + # if not common_domain: + # return result["total_items"] + # else: + # # TODO: bnioe sta be, perque el _exec_wcapi_call no retorna sempre tot!!! + # # cal unsa fucnio intermitja + # return len(self._filter(result['data'], common_domain)) def _get_search_fields(self): - return ["modified_after", "offset", "per_page", "page"] + return ["modified_after"] # , "offset", "per_page", "page"] - def _exec_get(self, resource, *args, **kwargs): + # TODO: User API params search and search_fields to find for some fields like + # name, etc. This is a ilike contain %name% search so we need _filter + # as well but on a lot less records + def _exec_get( + self, resource, domain=None, offset=0, limit=None, count=False + ): # noqa: C901 if resource == "system_status": - return self._exec_wcapi_call("get", resource, *args, **kwargs) + return self._exec_wcapi_call("get", resource) # , *args, **kwargs) # WooCommerce has the parameter next on the response headers # to get the next page but we can't use it because if we use # the offset, the next page will have the same items as the first page. # It looks like a bug in WooCommerce API. - domain = [] - if "domain" in kwargs: - domain = kwargs.pop("domain") + # So the 'page' parameter is incompatible with 'offset' parameter. + # If 'offset' and 'page' are used at the same this, only 'offset' + # will be taken into account and the 'page' will be ignored. + # get the domain + if domain is None: + domain = [] search_fields = self._get_search_fields() real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) - limit = kwargs.pop("limit", None) - if limit is None: - limit = self.get_total_items(resource, domain) + # get the api call parameters + params = self._domain_to_normalized_dict(real_domain) + # per_page (records per page) + if "per_page" in params: + raise ValidationError( + _( + "The 'per_page' parameter is managed automatically " + "in WooCommerce API calls. Do not use it " + "in the domain." + ) + ) + page_size = self.backend_record.page_size + if page_size > 0: + params["per_page"] = page_size + if count: + params["_fields"] = "id" # only need the ids to count all_data = [] - if limit > 0: - params = self._domain_to_normalized_dict(real_domain) - offset = kwargs.pop("offset", 0) or 0 - params["offset"] = offset if offset >= 0 else 0 - page_size = self.backend_record.page_size - params["per_page"] = limit if page_size < 0 else page_size - count = 0 - end = False - while not end: + counter = 0 + if offset <= 0: + page = 1 + while True: + res = self._exec_wcapi_call( + "get", + resource, + params={ + **params, + "page": page, + }, + ) + if res["returned_items"] == 0: + break + data = self._filter(res["data"], common_domain) + data_count = len(data) if limit is not None: - if count + page_size > limit: - diff = limit - count - if diff <= 0: - raise ValidationError( - _( - "Unexpected error in pagination diff: %s, count: %s, " - "page_size: %s, limit: %s, params: %s" - ) - % ( - diff, - count, - page_size, - limit, - params, - ) - ) - params["per_page"] = diff - end = True + if limit < 0: + limit = 0 + if (counter + data_count) >= limit: + diff = limit - counter + all_data += data[:diff] + counter += diff + break + counter += data_count + if not count: + all_data += data + page += 1 + else: + cur_offset = offset + while True: res = self._exec_wcapi_call( - "get", resource, params=params, *args, **kwargs + "get", + resource, + params={ + **params, + "offset": cur_offset, + }, ) - all_data += res["data"] - if params["per_page"] != len(res["data"]): - raise ValidationError( - _( - "Unexpected error in pagination. The number of items " - "retrieved is different than the number aked." - ) - ) - count += len(res["data"]) - params["offset"] += len(res["data"]) - return self._filter(all_data, common_domain) + if res["returned_items"] == 0: + break + data = self._filter(res["data"], common_domain) + data_count = len(data) + if limit is not None: + if limit < 0: + limit = 0 + if (counter + data_count) >= limit: + diff = limit - counter + all_data += data[:diff] + counter += diff + break + counter += data_count + if not count: + all_data += data + cur_offset += res["returned_items"] + + if count: + return counter + else: + return all_data + + # limit = kwargs.pop("limit", None) + # if limit is None: + # limit = self.get_total_items(resource, domain) + # + # all_data = [] + # if limit > 0: + # params = self._domain_to_normalized_dict(real_domain) + # offset = kwargs.pop("offset", 0) or 0 + # params["offset"] = offset if offset >= 0 else 0 + # page_size = self.backend_record.page_size + # params["per_page"] = limit if page_size < 0 else page_size + # count = 0 + # end = False + # while not end: + # if limit is not None: + # if count + page_size > limit: + # diff = limit - count + # if diff <= 0: + # raise ValidationError( + # _( + # "Unexpected error in pagination diff: %s, count: %s, " + # "page_size: %s, limit: %s, params: %s" + # ) + # % ( + # diff, + # count, + # page_size, + # limit, + # params, + # ) + # ) + # params["per_page"] = diff + # end = True + # res = self._exec_wcapi_call( + # "get", resource, params=params, *args, **kwargs + # ) + # all_data += res["data"] + # if params["per_page"] != len(res["data"]): + # raise ValidationError( + # _( + # "Unexpected error in pagination. The number of items " + # "retrieved is different than the number aked." + # ) + # ) + # count += len(res["data"]) + # params["offset"] += len(res["data"]) + # return self._filter(all_data, common_domain) def _exec_post(self, resource, *args, **kwargs): res = self._exec_wcapi_call( From 4173dc3964d5605685a1aba18e67a6d43c5907b5 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 24 Dec 2025 19:43:57 +0100 Subject: [PATCH 04/11] [IMP] connector_extension_woocommerce: new wcapi exec_get logic simplified but with bugs --- .../components/adapter.py | 149 +++++------------- 1 file changed, 37 insertions(+), 112 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index 001c05293..db5d593ba 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -113,6 +113,13 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 kwargs, ) + # WooCommerce has the parameter next on the response headers + # to get the next page but we can't use it because if we use + # the offset, the next page will have the same items as the first page. + # It looks like a bug in WooCommerce API. + # So the 'page' parameter is incompatible with 'offset' parameter. + # If 'offset' and 'page' are used at the same this, only 'offset' + # will be taken into account and the 'page' will be ignored. params = kwargs.get("params", {}) if {"page", "offset"}.issubset(params): raise ValidationError( @@ -264,19 +271,9 @@ def _get_search_fields(self): # TODO: User API params search and search_fields to find for some fields like # name, etc. This is a ilike contain %name% search so we need _filter # as well but on a lot less records - def _exec_get( - self, resource, domain=None, offset=0, limit=None, count=False - ): # noqa: C901 + def _exec_get(self, resource, domain=None, offset=0, limit=None, count=False): if resource == "system_status": return self._exec_wcapi_call("get", resource) # , *args, **kwargs) - # WooCommerce has the parameter next on the response headers - # to get the next page but we can't use it because if we use - # the offset, the next page will have the same items as the first page. - # It looks like a bug in WooCommerce API. - # So the 'page' parameter is incompatible with 'offset' parameter. - # If 'offset' and 'page' are used at the same this, only 'offset' - # will be taken into account and the 'page' will be ignored. - # get the domain if domain is None: domain = [] @@ -297,119 +294,47 @@ def _exec_get( page_size = self.backend_record.page_size if page_size > 0: params["per_page"] = page_size + if offset < 0: + offset = 0 if count: params["_fields"] = "id" # only need the ids to count + seen_ids = set() all_data = [] counter = 0 - if offset <= 0: - page = 1 - while True: - res = self._exec_wcapi_call( - "get", - resource, - params={ - **params, - "page": page, - }, - ) - if res["returned_items"] == 0: - break - data = self._filter(res["data"], common_domain) - data_count = len(data) - if limit is not None: - if limit < 0: - limit = 0 - if (counter + data_count) >= limit: - diff = limit - counter - all_data += data[:diff] - counter += diff - break - counter += data_count - if not count: - all_data += data - page += 1 - else: - cur_offset = offset - while True: - res = self._exec_wcapi_call( - "get", - resource, - params={ - **params, - "offset": cur_offset, - }, - ) - if res["returned_items"] == 0: + while True: + res = self._exec_wcapi_call( + "get", + resource, + params={ + **params, + "offset": offset, + }, + ) + if res["returned_items"] == 0: + break + data = [d for d in res["data"] if d["id"] not in seen_ids] + seen_ids |= {d["id"] for d in data} + data = self._filter(data, *common_domain) + data_count = len(data) + if limit is not None: + if limit < 0: + limit = 0 + if (counter + data_count) >= limit: + diff = limit - counter + all_data += data[:diff] + counter += diff break - data = self._filter(res["data"], common_domain) - data_count = len(data) - if limit is not None: - if limit < 0: - limit = 0 - if (counter + data_count) >= limit: - diff = limit - counter - all_data += data[:diff] - counter += diff - break - counter += data_count - if not count: - all_data += data - cur_offset += res["returned_items"] + counter += data_count + if not count: + all_data += data + offset += res["returned_items"] if count: return counter else: return all_data - # limit = kwargs.pop("limit", None) - # if limit is None: - # limit = self.get_total_items(resource, domain) - # - # all_data = [] - # if limit > 0: - # params = self._domain_to_normalized_dict(real_domain) - # offset = kwargs.pop("offset", 0) or 0 - # params["offset"] = offset if offset >= 0 else 0 - # page_size = self.backend_record.page_size - # params["per_page"] = limit if page_size < 0 else page_size - # count = 0 - # end = False - # while not end: - # if limit is not None: - # if count + page_size > limit: - # diff = limit - count - # if diff <= 0: - # raise ValidationError( - # _( - # "Unexpected error in pagination diff: %s, count: %s, " - # "page_size: %s, limit: %s, params: %s" - # ) - # % ( - # diff, - # count, - # page_size, - # limit, - # params, - # ) - # ) - # params["per_page"] = diff - # end = True - # res = self._exec_wcapi_call( - # "get", resource, params=params, *args, **kwargs - # ) - # all_data += res["data"] - # if params["per_page"] != len(res["data"]): - # raise ValidationError( - # _( - # "Unexpected error in pagination. The number of items " - # "retrieved is different than the number aked." - # ) - # ) - # count += len(res["data"]) - # params["offset"] += len(res["data"]) - # return self._filter(all_data, common_domain) - def _exec_post(self, resource, *args, **kwargs): res = self._exec_wcapi_call( "post", From 60b0a3706f037644cc57db13b0fcf4c868c5d923 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 24 Dec 2025 21:39:00 +0100 Subject: [PATCH 05/11] [FIX] connector_extension_woocommerce: offset and limit working --- .../components/adapter.py | 82 ++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index db5d593ba..0376a32fe 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -1,4 +1,4 @@ -# Copyright NuoBiT Solutions - Eric Antones +# Copyright 2025 NuoBiT Solutions - Eric Antones # Copyright NuoBiT Solutions - Kilian Niubo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) @@ -106,7 +106,8 @@ def _manage_error_codes( def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 if self.backend_record.enable_call_logging: _logger.info( - "WooCommerce API Call - OP: %s, Resource: %s, Args: %s, KWArgs: %s", + "WooCommerce API Call (_exec_wcapi_call) - OP: %s, " + "Resource: %s, Args: %s, KWArgs: %s", op, resource, args, @@ -244,8 +245,10 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 if self.backend_record.enable_call_logging: result_debug = dict(result) - result_debug.pop("data") - _logger.info("WooCommerce API Response: %s", result_debug) + result_debug["data"] = "..." + _logger.info( + "WooCommerce API Response (_exec_wcapi_call) - Result: %s", result_debug + ) return result # def get_total_items(self, resource, domain=None): @@ -271,12 +274,27 @@ def _get_search_fields(self): # TODO: User API params search and search_fields to find for some fields like # name, etc. This is a ilike contain %name% search so we need _filter # as well but on a lot less records - def _exec_get(self, resource, domain=None, offset=0, limit=None, count=False): + def _exec_get( + self, + resource, + domain=None, + offset=0, + limit=None, + count=False, + ): + # flake8: noqa: C901 if resource == "system_status": return self._exec_wcapi_call("get", resource) # , *args, **kwargs) - # get the domain + # prepare parameters if domain is None: domain = [] + if limit is not None: + if limit < 0: + limit = 0 + if offset < 0: + offset = 0 + + # get domains search_fields = self._get_search_fields() real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) @@ -294,42 +312,68 @@ def _exec_get(self, resource, domain=None, offset=0, limit=None, count=False): page_size = self.backend_record.page_size if page_size > 0: params["per_page"] = page_size - if offset < 0: - offset = 0 if count: params["_fields"] = "id" # only need the ids to count seen_ids = set() all_data = [] counter = 0 + page = 1 + api_calls = 0 while True: res = self._exec_wcapi_call( "get", resource, params={ **params, - "offset": offset, + "page": page, }, ) + api_calls += 1 if res["returned_items"] == 0: break data = [d for d in res["data"] if d["id"] not in seen_ids] + data = self._filter(data, common_domain) seen_ids |= {d["id"] for d in data} - data = self._filter(data, *common_domain) + + # compute offset + if data and offset > 0: + data_count = len(data) + if offset < data_count: + data = data[offset:] + offset = 0 + else: + data = [] + offset -= data_count + data_count = len(data) - if limit is not None: - if limit < 0: - limit = 0 - if (counter + data_count) >= limit: - diff = limit - counter - all_data += data[:diff] - counter += diff - break + + # compute limit + if limit is not None and (counter + data_count) >= limit: + diff = limit - counter + all_data += data[:diff] + counter += diff + break counter += data_count if not count: all_data += data - offset += res["returned_items"] + if page >= res["total_pages"]: + break + page += 1 + if self.backend_record.enable_call_logging: + _logger.info( + "WooCommerce API Response (_exec_get) - OP: %s, Resource: %s, Domain: %s, " + "Offset: %s, Limit: %s, Count: %s, Params: %s, API Calls: %s", + "GET", + resource, + domain, + offset, + limit, + count, + params, + api_calls, + ) if count: return counter else: From c1ffe8e45a80a71e367fd87d378d261b707f1526 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Fri, 26 Dec 2025 18:46:05 +0100 Subject: [PATCH 06/11] [FIX] connector_extension_woocommerce: error checking woocomerce coonnection --- connector_extension_woocommerce/components/adapter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index 0376a32fe..a1966b1ff 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -406,5 +406,10 @@ def get_version(self): system_status = self._exec("get", "system_status") version = False if system_status: - version = system_status["data"].get("environment", {}).get("version") + if system_status.get("returned_items", 0) != 1: + raise ValidationError( + _("Unexpected response from WooCommerce system_status: %s") + % system_status + ) + version = system_status["data"][0].get("environment", {}).get("version") return version From 03a39cfc86e4ddb74786e96b19cb7d1e7d00dd35 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Tue, 27 Jan 2026 01:05:18 +0100 Subject: [PATCH 07/11] [FIX] connector_extension_woocommerce: wrong returned items computation and non-get requests are always a singleton (dict) --- .../components/adapter.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index a1966b1ff..1ac0e981a 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -202,26 +202,22 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 % (header, res_data) ) if singleton: - if op == "get": - result["data"] = [res_data] - else: - result["data"] = res_data + result["data"] = [res_data] result["total_items"] = 1 result["total_pages"] = 1 else: - if op == "get": - result["data"] = res_data - result["total_items"] = int(response.headers["X-WP-Total"]) - result["total_pages"] = int(response.headers["X-WP-TotalPages"]) - # if "next" in response.links: - # result["next"] = response.links["next"]["url"] - # if "prev" in response.links: - # result["prev"] = response.links["prev"]["url"] - else: + if op != "get": raise ValidationError( _("Unexpected multi-response for operation '%s': %s") % (op, res_data) ) + result["data"] = res_data + result["total_items"] = int(response.headers["X-WP-Total"]) + result["total_pages"] = int(response.headers["X-WP-TotalPages"]) + # if "next" in response.links: + # result["next"] = response.links["next"]["url"] + # if "prev" in response.links: + # result["prev"] = response.links["prev"]["url"] result["returned_items"] = len(result["data"]) except RequestConnectionError as e: raise RetryableJobError(_("Error connecting to WooCommerce: %s") % e) from e @@ -386,18 +382,33 @@ def _exec_post(self, resource, *args, **kwargs): *args, **kwargs, ) - return res["data"] + if res["total_items"] != 1: + raise ValidationError( + _("Unexpected response from WooCommerce POST %s: %s") % (resource, res) + ) + return res["data"][0] def _exec_put(self, resource, *args, **kwargs): - return self._exec_wcapi_call("put", resource, *args, **kwargs) + res = self._exec_wcapi_call("put", resource, *args, **kwargs) + if res["total_items"] != 1: + raise ValidationError( + _("Unexpected response from WooCommerce PUT %s: %s") % (resource, res) + ) + return res["data"][0] def _exec_delete(self, resource, *args, **kwargs): - return self._exec_wcapi_call( + res = self._exec_wcapi_call( "delete", resource, *args, **kwargs, ) + if res["total_items"] != 1: + raise ValidationError( + _("Unexpected response from WooCommerce DELETE %s: %s") + % (resource, res) + ) + return res["data"][0] def _exec_options(self, resource, *args, **kwargs): raise NotImplementedError() From b5e9056e0f239bf08de8e48c667847439e2be308 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Wed, 28 Jan 2026 01:25:07 +0100 Subject: [PATCH 08/11] [IMP] connector_extension_woocommerce: better error message exporting atribute values --- connector_extension_woocommerce/components/adapter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index 1ac0e981a..df1868f29 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -65,7 +65,11 @@ def _manage_error_codes( elif res.status_code == 400: if res_data.get("code") == "term_exists": error_message = _( - "Error: '%s'. Probably repeated record already exists in Woocommerce.\n" + "Error: '%s'. Probably repeated record already exists in Woocommerce. " + "If you don't see it from the interface probably the database is dirty " + "and you should check the database directly. It means that there's " + "terms (value attributes) in table wp_term_taxonomy with a" + "non-existent taxonomy (attribute).\n" "Please, review the data in %s/%s and compare it with %s" % ( res_data["message"], From e8aa6375a2a5a02c8803c5ebdab1bf2331d172b4 Mon Sep 17 00:00:00 2001 From: Eric Antones Date: Fri, 13 Feb 2026 09:41:57 +0100 Subject: [PATCH 09/11] [FIX+IMP] connector_extension_woocommerce: implement count records + optimize WooCommerce API calls for item counting and pagination --- .../components/adapter.py | 99 ++++++++++++------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index df1868f29..7e21c6ea3 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -97,14 +97,6 @@ def _manage_error_codes( return error_message return res_data - # # TODO: remove this total items and use the res.headers instead - # def _get_res_total_items(self, res): - # headers = res.headers - # total_items = headers.get("X-WP-Total") or 0 - # if total_items: - # total_items = int(headers.get("X-WP-Total")) - # return total_items - # TODO: Remove *args and *kwargs and put params=None # Check other methods than get to see if it'll work for them too def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 @@ -123,7 +115,7 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 # the offset, the next page will have the same items as the first page. # It looks like a bug in WooCommerce API. # So the 'page' parameter is incompatible with 'offset' parameter. - # If 'offset' and 'page' are used at the same this, only 'offset' + # If 'offset' and 'page' are used at the same time, only 'offset' # will be taken into account and the 'page' will be ignored. params = kwargs.get("params", {}) if {"page", "offset"}.issubset(params): @@ -251,22 +243,20 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 ) return result - # def get_total_items(self, resource, domain=None): - # filters_values = self._get_search_fields() - # real_domain, common_domain = self._extract_domain_clauses( - # domain, filters_values - # ) - # params = self._domain_to_normalized_dict(real_domain) - # if not common_domain: - # params["per_page"] = 1 - # result = self._exec_wcapi_call("get", resource, params=params) - # - # if not common_domain: - # return result["total_items"] - # else: - # # TODO: bnioe sta be, perque el _exec_wcapi_call no retorna sempre tot!!! - # # cal unsa fucnio intermitja - # return len(self._filter(result['data'], common_domain)) + def get_total_items(self, resource, domain=None): + """ + Get the total count of items matching the given domain. + + This method leverages the optimized _exec_get count mode which: + - Makes only 1 API call when no local filtering is needed + - Extracts count from X-WP-Total header + - Minimizes data transfer (per_page=1, _fields=id) + + :param resource: WooCommerce API endpoint (e.g., "products", "orders") + :param domain: Filter conditions (Odoo-style domain) + :return: Total count of matching items (integer) + """ + return self._exec_get(resource, domain=domain, count=True) def _get_search_fields(self): return ["modified_after"] # , "offset", "per_page", "page"] @@ -282,6 +272,26 @@ def _exec_get( limit=None, count=False, ): + """ + Execute GET operation with pagination, filtering, and optional counting. + + WooCommerce API Bug: 'offset' and 'page' parameters are incompatible. + Large offsets require multiple API calls (offset=1000 → ~100 calls). + Prefer date filters like ('modified_after', '>=', '2026-01-01'). + + Domain filtering: API-supported fields (api_domain) are sent to WooCommerce, + unsupported fields (local_domain) are filtered locally after fetch. + + Count optimization: When count=True and no local filtering needed, + makes 1 API call with per_page=1 and extracts count from headers. + + :param resource: WooCommerce endpoint (e.g., "products", "orders") + :param domain: Odoo-style domain filters + :param offset: Records to skip (simulated via pagination) + :param limit: Max records to return + :param count: If True, return count; if False, return data + :return: list[dict] or int + """ # flake8: noqa: C901 if resource == "system_status": return self._exec_wcapi_call("get", resource) # , *args, **kwargs) @@ -295,11 +305,14 @@ def _exec_get( offset = 0 # get domains + # Separate domain into: + # - api_domain: Domain clauses supported natively by WooCommerce API + # - local_domain: Domain clauses that must be applied locally in Odoo after fetching search_fields = self._get_search_fields() - real_domain, common_domain = self._extract_domain_clauses(domain, search_fields) + api_domain, local_domain = self._extract_domain_clauses(domain, search_fields) # get the api call parameters - params = self._domain_to_normalized_dict(real_domain) + params = self._domain_to_normalized_dict(api_domain) # per_page (records per page) if "per_page" in params: raise ValidationError( @@ -313,8 +326,14 @@ def _exec_get( if page_size > 0: params["per_page"] = page_size if count: + # Optimization: If no local filtering, only fetch 1 record to minimize data transfer + # We only need the total count from headers (X-WP-Total) + if not local_domain: + params["per_page"] = 1 params["_fields"] = "id" # only need the ids to count + # Deduplication protection: WooCommerce API may return duplicate IDs + # in some edge cases (e.g., when items are modified during pagination) seen_ids = set() all_data = [] counter = 0 @@ -330,29 +349,41 @@ def _exec_get( }, ) api_calls += 1 + + # Optimization: If counting with no local filtering, use total from API headers + if count and not local_domain: + counter = res["total_items"] + break + if res["returned_items"] == 0: break data = [d for d in res["data"] if d["id"] not in seen_ids] - data = self._filter(data, common_domain) + data = self._filter(data, local_domain) seen_ids |= {d["id"] for d in data} - # compute offset + # Compute offset (simulate offset by discarding records from early pages) + # This is necessary because WooCommerce's offset parameter is incompatible with page parameter if data and offset > 0: data_count = len(data) if offset < data_count: + # We've reached the page containing the offset position + # Keep only records after the offset data = data[offset:] - offset = 0 + offset = 0 # Reset offset since we've consumed it else: + # This entire page is before the offset position + # Discard all records and reduce offset by page size data = [] offset -= data_count data_count = len(data) - # compute limit + # Compute limit (stop when we've collected enough records) if limit is not None and (counter + data_count) >= limit: - diff = limit - counter - all_data += data[:diff] - counter += diff + # We have more records than needed, take only what's required + remaining_items = limit - counter + all_data += data[:remaining_items] + counter += remaining_items break counter += data_count if not count: From 3d24190f98f9a4e796e0d871de61a324442b6854 Mon Sep 17 00:00:00 2001 From: ??? Date: Fri, 27 Mar 2026 13:05:25 +0100 Subject: [PATCH 10/11] [IMP] connector_extension_woocommerce: pre-commit auto fixes --- connector_extension_woocommerce/README.rst | 21 ++++++++++--------- .../__manifest__.py | 2 +- .../pyproject.toml | 3 +++ .../readme/CONTRIBUTORS.md | 3 +++ .../readme/CONTRIBUTORS.rst | 4 ---- .../{DESCRIPTION.rst => DESCRIPTION.md} | 3 ++- .../static/description/index.html | 17 ++++++++------- 7 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 connector_extension_woocommerce/pyproject.toml create mode 100644 connector_extension_woocommerce/readme/CONTRIBUTORS.md delete mode 100644 connector_extension_woocommerce/readme/CONTRIBUTORS.rst rename connector_extension_woocommerce/readme/{DESCRIPTION.rst => DESCRIPTION.md} (78%) diff --git a/connector_extension_woocommerce/README.rst b/connector_extension_woocommerce/README.rst index 4059706b0..c71be579c 100644 --- a/connector_extension_woocommerce/README.rst +++ b/connector_extension_woocommerce/README.rst @@ -17,12 +17,13 @@ Connector Extension Woocommerce :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-NuoBiT%2Fodoo--addons-lightgray.png?logo=github - :target: https://github.com/NuoBiT/odoo-addons/tree/14.0/connector_extension_woocommerce + :target: https://github.com/NuoBiT/odoo-addons/tree/18.0/connector_extension_woocommerce :alt: NuoBiT/odoo-addons |badge1| |badge2| |badge3| -This module extends the connector extension module to add support for Woocommerce +This module extends the connector extension module to add support for +Woocommerce **Table of contents** @@ -35,7 +36,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -43,22 +44,22 @@ Credits ======= Authors -~~~~~~~ +------- * NuoBiT Solutions * SL Contributors -~~~~~~~~~~~~ +------------ -* `NuoBiT `__: +- `NuoBiT `__: - * Kilian Niubo - * Eric Antones + - Kilian Niubo + - Eric Antones Maintainers -~~~~~~~~~~~ +----------- -This module is part of the `NuoBiT/odoo-addons `_ project on GitHub. +This module is part of the `NuoBiT/odoo-addons `_ project on GitHub. You are welcome to contribute. diff --git a/connector_extension_woocommerce/__manifest__.py b/connector_extension_woocommerce/__manifest__.py index 1167367b1..5a0328a80 100644 --- a/connector_extension_woocommerce/__manifest__.py +++ b/connector_extension_woocommerce/__manifest__.py @@ -10,6 +10,6 @@ "author": "NuoBiT Solutions, SL", "license": "LGPL-3", "category": "Connector", - "website": "https://github.com/nuobit/odoo-addons", + "website": "https://github.com/NuoBiT/odoo-addons", "depends": ["connector_extension"], } diff --git a/connector_extension_woocommerce/pyproject.toml b/connector_extension_woocommerce/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/connector_extension_woocommerce/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/connector_extension_woocommerce/readme/CONTRIBUTORS.md b/connector_extension_woocommerce/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..e861d9e77 --- /dev/null +++ b/connector_extension_woocommerce/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [NuoBiT](https://www.nuobit.com): + - Kilian Niubo \ + - Eric Antones \ diff --git a/connector_extension_woocommerce/readme/CONTRIBUTORS.rst b/connector_extension_woocommerce/readme/CONTRIBUTORS.rst deleted file mode 100644 index e468d95a0..000000000 --- a/connector_extension_woocommerce/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,4 +0,0 @@ -* `NuoBiT `__: - - * Kilian Niubo - * Eric Antones diff --git a/connector_extension_woocommerce/readme/DESCRIPTION.rst b/connector_extension_woocommerce/readme/DESCRIPTION.md similarity index 78% rename from connector_extension_woocommerce/readme/DESCRIPTION.rst rename to connector_extension_woocommerce/readme/DESCRIPTION.md index cf1445fe3..6f0e3d838 100644 --- a/connector_extension_woocommerce/readme/DESCRIPTION.rst +++ b/connector_extension_woocommerce/readme/DESCRIPTION.md @@ -1 +1,2 @@ -This module extends the connector extension module to add support for Woocommerce +This module extends the connector extension module to add support for +Woocommerce diff --git a/connector_extension_woocommerce/static/description/index.html b/connector_extension_woocommerce/static/description/index.html index b059823a1..580c7d706 100644 --- a/connector_extension_woocommerce/static/description/index.html +++ b/connector_extension_woocommerce/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -369,8 +369,9 @@

Connector Extension Woocommerce

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:f381547277a60fe3768f6861b4c213c845b21507225ce930457b473de3c08289 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: LGPL-3 NuoBiT/odoo-addons

-

This module extends the connector extension module to add support for Woocommerce

+

Beta License: LGPL-3 NuoBiT/odoo-addons

+

This module extends the connector extension module to add support for +Woocommerce

Table of contents

    @@ -388,7 +389,7 @@

    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.

    +feedback.

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

@@ -412,7 +413,7 @@

Contributors

Maintainers

-

This module is part of the NuoBiT/odoo-addons project on GitHub.

+

This module is part of the NuoBiT/odoo-addons project on GitHub.

You are welcome to contribute.

From 58f4ab467eb9acf68b7e9404f53ab3d68e3bca97 Mon Sep 17 00:00:00 2001 From: ??? Date: Fri, 27 Mar 2026 13:12:58 +0100 Subject: [PATCH 11/11] [MIG] connector_extension_woocommerce: Migration to 18.0 --- connector_extension_woocommerce/README.rst | 8 +- .../__manifest__.py | 5 +- .../components/adapter.py | 158 ++++++++++++------ .../pyproject.toml | 1 + .../readme/CONTRIBUTORS.md | 5 +- .../readme/DESCRIPTION.md | 3 +- .../static/description/index.html | 8 +- 7 files changed, 122 insertions(+), 66 deletions(-) diff --git a/connector_extension_woocommerce/README.rst b/connector_extension_woocommerce/README.rst index c71be579c..0d5d30100 100644 --- a/connector_extension_woocommerce/README.rst +++ b/connector_extension_woocommerce/README.rst @@ -46,16 +46,16 @@ Credits Authors ------- -* NuoBiT Solutions -* SL +* NuoBiT Solutions SL Contributors ------------ - `NuoBiT `__: - - Kilian Niubo - - Eric Antones + - Kilian Niubo kniubo@nuobit.com + - Eric Antones eantones@nuobit.com + - Deniz Gallo dgallo@nuobit.com Maintainers ----------- diff --git a/connector_extension_woocommerce/__manifest__.py b/connector_extension_woocommerce/__manifest__.py index 5a0328a80..dc80943b1 100644 --- a/connector_extension_woocommerce/__manifest__.py +++ b/connector_extension_woocommerce/__manifest__.py @@ -1,13 +1,14 @@ # Copyright NuoBiT Solutions - Kilian Niubo # Copyright NuoBiT Solutions - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) { "name": "Connector Extension Woocommerce", "summary": "This module extends the connector extension module " "to add support for Woocommerce", - "version": "14.0.1.0.0", - "author": "NuoBiT Solutions, SL", + "version": "18.0.1.0.0", + "author": "NuoBiT Solutions SL", "license": "LGPL-3", "category": "Connector", "website": "https://github.com/NuoBiT/odoo-addons", diff --git a/connector_extension_woocommerce/components/adapter.py b/connector_extension_woocommerce/components/adapter.py index 7e21c6ea3..02c841e59 100644 --- a/connector_extension_woocommerce/components/adapter.py +++ b/connector_extension_woocommerce/components/adapter.py @@ -1,5 +1,6 @@ # Copyright 2025 NuoBiT Solutions - Eric Antones # Copyright NuoBiT Solutions - Kilian Niubo +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) import json @@ -25,7 +26,7 @@ class ConnectorExtensionWooCommerceAdapterCRUD(AbstractComponent): def _exec(self, op, resource, *args, **kwargs): if kwargs.get("domain"): kwargs["domain"] = trim_domain(kwargs["domain"]) - func = getattr(self, "_exec_%s" % op) + func = getattr(self, f"_exec_{op}") return func(resource, *args, **kwargs) def _manage_error_codes( @@ -36,11 +37,14 @@ def _manage_error_codes( if res.status_code == 404: if res_data.get("code") == "rest_no_route": error_message = _( - "Error: '%s'. Probably the %s has been" + "Error: '%(message)s'. Probably the %(resource)s has been" " removed from Woocommerce. " - "If it's the case, try to remove the binding of the %s." - % (res_data.get("message"), resource, self.model._name) - ) + "If it's the case, try to remove the binding of the %(model)s." + ) % { + "message": res_data.get("message"), + "resource": resource, + "model": self.model._name, + } # elif res_data.get("code") == "woocommerce_rest_term_invalid": # error_message = _( # "Error: '%s'. Probably the %s has been " @@ -65,33 +69,42 @@ def _manage_error_codes( elif res.status_code == 400: if res_data.get("code") == "term_exists": error_message = _( - "Error: '%s'. Probably repeated record already exists in Woocommerce. " - "If you don't see it from the interface probably the database is dirty " - "and you should check the database directly. It means that there's " - "terms (value attributes) in table wp_term_taxonomy with a" - "non-existent taxonomy (attribute).\n" - "Please, review the data in %s/%s and compare it with %s" - % ( - res_data["message"], - resource, - res_data["data"]["resource_id"], - kwargs["data"], - ) - ) + "Error: '%(message)s'. Probably repeated record already " + "exists in Woocommerce. " + "If you don't see it from the interface probably the database" + " is dirty and you should check the database directly. It means" + " that there's terms (value attributes) in table " + "wp_term_taxonomy with a non-existent taxonomy (attribute).\n" + "Please, review the data in %(resource)s/%(resource_id)s " + "and compare it with %(data)s" + ) % { + "message": res_data["message"], + "resource": resource, + "resource_id": res_data["data"]["resource_id"], + "data": kwargs["data"], + } # elif res_data.get("code") in [ # "woocommerce_rest_product_variation_invalid_id", # "woocommerce_rest_product_invalid_id", # ]: # error_message = _( - # "Error: '%s'. Probably the %s has been removed from Woocommerce. " + # "Error: '%s'. Probably the %s has been + # removed from Woocommerce. " # "If it's the case, try to remove the binding of the %s." # % (res_data.get("message"), resource, self.model._name) # ) if not error_message: error_message = _( - "Error: %s -> Op: %s, Resource: %s, Args: %s, KWArgs: %s" - % (res_data, op, resource, args, kwargs) - ) + "Error: %(data)s -> Op: %(op)s, Resource: %(resource)s, " + "Args: %(args)s, KWArgs: %(kwargs)s" + ) % { + "data": res_data, + "op": op, + "resource": resource, + "args": args, + "kwargs": kwargs, + } + if raise_on_error: raise ValidationError(error_message) return error_message @@ -141,8 +154,8 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 if not response.ok: # These are the cases where a not found should be an empty result # instead of an error, we need to bypass the standard REST behaviour - # and make it look like more like an SQL whre if the parameters are wrong - # it just returns no value + # and make it look like more like an SQL whre if the parameters are + # wrong it just returns no value if op == "get": if response.status_code != 404 or res_data["code"] not in ( "woocommerce_rest_product_invalid_id", @@ -176,16 +189,20 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 if header in response.headers: raise ValidationError( _( - "The '%s' header should not be present in " - "singleton responses: %s" + "The '%(header)s' header should not be present in " + "singleton responses: %(response)s" ) - % (header, res_data) + % { + "header": header, + "response": res_data, + } ) # for link in response.links.keys(): # if link in ["next", "prev", "first", "last"]: # raise ValidationError( # _( - # "The '%s' link should not be present in singleton " + # "The '%s' link should not be + # present in singleton " # "responses: %s" # ) # % (link, res_data) @@ -194,8 +211,14 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 for header in multi_headers: if header not in response.headers: raise ValidationError( - _("The '%s' header is missing in multi responses: %s") - % (header, res_data) + _( + "The '%(header)s' header is missing" + " in multi responses: %(response)s" + ) + % { + "header": header, + "response": res_data, + } ) if singleton: result["data"] = [res_data] @@ -204,8 +227,14 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 else: if op != "get": raise ValidationError( - _("Unexpected multi-response for operation '%s': %s") - % (op, res_data) + _( + "Unexpected multi-response for " + "operation '%(op)s': %(response)s" + ) + % { + "op": op, + "response": res_data, + } ) result["data"] = res_data result["total_items"] = int(response.headers["X-WP-Total"]) @@ -221,18 +250,18 @@ def _exec_wcapi_call(self, op, resource, *args, **kwargs): # noqa: C901 raise ValidationError( _( "Error decoding json WooCommerce response: " - "%s\nArgs:%s\nKwargs:%s\n" - "URL:%s\nHeaders:%s\nMethod:%s\nBody:%s" - ) - % ( - e, - args, - kwargs, - response.url, - response.request.headers, - response.request.method, - response.text and response.text[:100] + " ...", + "%(error)s\nArgs:%(args)s\nKwargs:%(kwargs)s\n" + "URL:%(url)s\nHeaders:%(headers)s\nMethod:%(method)s\nBody:%(body)s" ) + % { + "error": e, + "args": args, + "kwargs": kwargs, + "url": response.url, + "headers": response.request.headers, + "method": response.request.method, + "body": response.text and response.text[:100] + " ...", + } ) from e if self.backend_record.enable_call_logging: @@ -307,7 +336,8 @@ def _exec_get( # get domains # Separate domain into: # - api_domain: Domain clauses supported natively by WooCommerce API - # - local_domain: Domain clauses that must be applied locally in Odoo after fetching + # - local_domain: Domain clauses that must be applied locally in + # Odoo after fetching search_fields = self._get_search_fields() api_domain, local_domain = self._extract_domain_clauses(domain, search_fields) @@ -326,7 +356,8 @@ def _exec_get( if page_size > 0: params["per_page"] = page_size if count: - # Optimization: If no local filtering, only fetch 1 record to minimize data transfer + # Optimization: If no local filtering, only fetch 1 record + # to minimize data transfer # We only need the total count from headers (X-WP-Total) if not local_domain: params["per_page"] = 1 @@ -350,7 +381,8 @@ def _exec_get( ) api_calls += 1 - # Optimization: If counting with no local filtering, use total from API headers + # Optimization: If counting with no local filtering, + # use total from API headers if count and not local_domain: counter = res["total_items"] break @@ -362,7 +394,8 @@ def _exec_get( seen_ids |= {d["id"] for d in data} # Compute offset (simulate offset by discarding records from early pages) - # This is necessary because WooCommerce's offset parameter is incompatible with page parameter + # This is necessary because WooCommerce's offset + # parameter is incompatible with page parameter if data and offset > 0: data_count = len(data) if offset < data_count: @@ -394,7 +427,8 @@ def _exec_get( if self.backend_record.enable_call_logging: _logger.info( - "WooCommerce API Response (_exec_get) - OP: %s, Resource: %s, Domain: %s, " + "WooCommerce API Response (_exec_get) " + "- OP: %s, Resource: %s, Domain: %s, " "Offset: %s, Limit: %s, Count: %s, Params: %s, API Calls: %s", "GET", resource, @@ -419,7 +453,14 @@ def _exec_post(self, resource, *args, **kwargs): ) if res["total_items"] != 1: raise ValidationError( - _("Unexpected response from WooCommerce POST %s: %s") % (resource, res) + _( + "Unexpected response from WooCommerce " + "POST %(resource)s: (response))s" + ) + % { + "resource": resource, + "response": res, + } ) return res["data"][0] @@ -427,7 +468,14 @@ def _exec_put(self, resource, *args, **kwargs): res = self._exec_wcapi_call("put", resource, *args, **kwargs) if res["total_items"] != 1: raise ValidationError( - _("Unexpected response from WooCommerce PUT %s: %s") % (resource, res) + _( + "Unexpected response from WooCommerce " + "PUT %(resource)s: %(response)s" + ) + % { + "resource": resource, + "response": res, + } ) return res["data"][0] @@ -440,8 +488,14 @@ def _exec_delete(self, resource, *args, **kwargs): ) if res["total_items"] != 1: raise ValidationError( - _("Unexpected response from WooCommerce DELETE %s: %s") - % (resource, res) + _( + "Unexpected response from WooCommerce " + "DELETE %(resource)s: %(response)s" + ) + % { + "resource": resource, + "response": res, + } ) return res["data"][0] diff --git a/connector_extension_woocommerce/pyproject.toml b/connector_extension_woocommerce/pyproject.toml index 4231d0ccc..f759fc0b0 100644 --- a/connector_extension_woocommerce/pyproject.toml +++ b/connector_extension_woocommerce/pyproject.toml @@ -1,3 +1,4 @@ + [build-system] requires = ["whool"] build-backend = "whool.buildapi" diff --git a/connector_extension_woocommerce/readme/CONTRIBUTORS.md b/connector_extension_woocommerce/readme/CONTRIBUTORS.md index e861d9e77..0355cecbc 100644 --- a/connector_extension_woocommerce/readme/CONTRIBUTORS.md +++ b/connector_extension_woocommerce/readme/CONTRIBUTORS.md @@ -1,3 +1,4 @@ - [NuoBiT](https://www.nuobit.com): - - Kilian Niubo \ - - Eric Antones \ + - Kilian Niubo + - Eric Antones + - Deniz Gallo diff --git a/connector_extension_woocommerce/readme/DESCRIPTION.md b/connector_extension_woocommerce/readme/DESCRIPTION.md index 6f0e3d838..cf1445fe3 100644 --- a/connector_extension_woocommerce/readme/DESCRIPTION.md +++ b/connector_extension_woocommerce/readme/DESCRIPTION.md @@ -1,2 +1 @@ -This module extends the connector extension module to add support for -Woocommerce +This module extends the connector extension module to add support for Woocommerce diff --git a/connector_extension_woocommerce/static/description/index.html b/connector_extension_woocommerce/static/description/index.html index 580c7d706..17a7a7b18 100644 --- a/connector_extension_woocommerce/static/description/index.html +++ b/connector_extension_woocommerce/static/description/index.html @@ -397,16 +397,16 @@

Credits

Authors

    -
  • NuoBiT Solutions
  • -
  • SL
  • +
  • NuoBiT Solutions SL

Contributors