From c9749dd185db951c691b16e1524b57f058767c8d Mon Sep 17 00:00:00 2001
From: Florent THOMAS
Date: Mon, 22 Aug 2022 16:41:02 +0200
Subject: [PATCH] ADD catalog-maanger
---
.../README.rst | 74 ++++
.../__init__.py | 4 +
.../__manifest__.py | 37 ++
.../models/__init__.py | 10 +
.../models/binding/__init__.py | 4 +
.../models/binding/common.py | 16 +
.../models/ir_translation.py | 46 +++
.../models/product_brand/__init__.py | 6 +
.../models/product_brand/common.py | 49 +++
.../models/product_brand/deleter.py | 12 +
.../models/product_brand/exporter.py | 25 ++
.../models/product_category/__init__.py | 3 +
.../models/product_category/common.py | 141 ++++++++
.../models/product_category/deleter.py | 13 +
.../models/product_category/exporter.py | 99 ++++++
.../models/product_image/__init__.py | 3 +
.../models/product_image/common.py | 51 +++
.../models/product_image/deleter.py | 10 +
.../models/product_image/exporter.py | 162 +++++++++
.../models/product_product/__init__.py | 3 +
.../models/product_product/common.py | 160 +++++++++
.../models/product_product/deleter.py | 19 +
.../models/product_product/exporter.py | 293 +++++++++++++++
.../models/product_template/__init__.py | 3 +
.../models/product_template/common.py | 125 +++++++
.../models/product_template/deleter.py | 13 +
.../models/product_template/exporter.py | 334 ++++++++++++++++++
.../readme/CREDITS.rst | 1 +
.../readme/USAGE.rst | 3 +
.../security/ir.model.access.csv | 8 +
.../static/description/icon.png | Bin 0 -> 8227 bytes
.../static/description/icon.svg | 238 +++++++++++++
.../tests/__init__.py | 6 +
.../tests/common.py | 30 ++
.../test_export_product_attribute.yaml | 50 +++
.../test_export_product_attribute_value.yaml | 46 +++
.../cassettes/test_export_product_brand.yaml | 54 +++
.../test_export_product_category.yaml | 69 ++++
.../cassettes/test_export_product_image.yaml | 225 ++++++++++++
.../test_export_product_product.yaml | 77 ++++
.../test_export_product_template.yaml | 110 ++++++
.../tests/test_export_product_attribute.py | 190 ++++++++++
.../tests/test_export_product_brand.py | 93 +++++
.../tests/test_export_product_category.py | 124 +++++++
.../tests/test_export_product_image.py | 147 ++++++++
.../tests/test_export_product_product.py | 228 ++++++++++++
.../tests/test_export_product_template.py | 266 ++++++++++++++
.../views/product_attribute_view.xml | 41 +++
.../views/product_category_view.xml | 18 +
.../views/product_image_view.xml | 13 +
.../views/product_view.xml | 46 +++
.../wizards/__init__.py | 7 +
.../wizards/active_deactive_products.py | 29 ++
.../wizards/active_deactive_products_view.xml | 72 ++++
.../wizards/export_brand.py | 45 +++
.../wizards/export_brand_view.xml | 38 ++
.../wizards/export_category.py | 49 +++
.../wizards/export_category_view.xml | 35 ++
.../wizards/export_multiple_products.py | 157 ++++++++
.../wizards/export_multiple_products_view.xml | 63 ++++
.../wizards/sync_products.py | 29 ++
.../wizards/sync_products_view.xml | 34 ++
62 files changed, 4356 insertions(+)
create mode 100644 connector_prestashop_catalog_manager/README.rst
create mode 100644 connector_prestashop_catalog_manager/__init__.py
create mode 100644 connector_prestashop_catalog_manager/__manifest__.py
create mode 100644 connector_prestashop_catalog_manager/models/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/binding/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/binding/common.py
create mode 100644 connector_prestashop_catalog_manager/models/ir_translation.py
create mode 100644 connector_prestashop_catalog_manager/models/product_brand/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/product_brand/common.py
create mode 100644 connector_prestashop_catalog_manager/models/product_brand/deleter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_brand/exporter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_category/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/product_category/common.py
create mode 100644 connector_prestashop_catalog_manager/models/product_category/deleter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_category/exporter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_image/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/product_image/common.py
create mode 100644 connector_prestashop_catalog_manager/models/product_image/deleter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_image/exporter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_product/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/product_product/common.py
create mode 100644 connector_prestashop_catalog_manager/models/product_product/deleter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_product/exporter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_template/__init__.py
create mode 100644 connector_prestashop_catalog_manager/models/product_template/common.py
create mode 100644 connector_prestashop_catalog_manager/models/product_template/deleter.py
create mode 100644 connector_prestashop_catalog_manager/models/product_template/exporter.py
create mode 100644 connector_prestashop_catalog_manager/readme/CREDITS.rst
create mode 100644 connector_prestashop_catalog_manager/readme/USAGE.rst
create mode 100644 connector_prestashop_catalog_manager/security/ir.model.access.csv
create mode 100644 connector_prestashop_catalog_manager/static/description/icon.png
create mode 100644 connector_prestashop_catalog_manager/static/description/icon.svg
create mode 100644 connector_prestashop_catalog_manager/tests/__init__.py
create mode 100644 connector_prestashop_catalog_manager/tests/common.py
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute_value.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_brand.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_category.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_image.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_product.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_template.yaml
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_attribute.py
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_brand.py
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_category.py
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_image.py
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_product.py
create mode 100644 connector_prestashop_catalog_manager/tests/test_export_product_template.py
create mode 100644 connector_prestashop_catalog_manager/views/product_attribute_view.xml
create mode 100644 connector_prestashop_catalog_manager/views/product_category_view.xml
create mode 100644 connector_prestashop_catalog_manager/views/product_image_view.xml
create mode 100644 connector_prestashop_catalog_manager/views/product_view.xml
create mode 100644 connector_prestashop_catalog_manager/wizards/__init__.py
create mode 100644 connector_prestashop_catalog_manager/wizards/active_deactive_products.py
create mode 100644 connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml
create mode 100644 connector_prestashop_catalog_manager/wizards/export_brand.py
create mode 100644 connector_prestashop_catalog_manager/wizards/export_brand_view.xml
create mode 100644 connector_prestashop_catalog_manager/wizards/export_category.py
create mode 100644 connector_prestashop_catalog_manager/wizards/export_category_view.xml
create mode 100644 connector_prestashop_catalog_manager/wizards/export_multiple_products.py
create mode 100644 connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
create mode 100644 connector_prestashop_catalog_manager/wizards/sync_products.py
create mode 100644 connector_prestashop_catalog_manager/wizards/sync_products_view.xml
diff --git a/connector_prestashop_catalog_manager/README.rst b/connector_prestashop_catalog_manager/README.rst
new file mode 100644
index 000000000..8582f7c74
--- /dev/null
+++ b/connector_prestashop_catalog_manager/README.rst
@@ -0,0 +1,74 @@
+.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+
+=============================================
+Catalog Manager for Odoo PrestaShop Connector
+=============================================
+
+This module is an extension for *connector_prestashop*. With it, you will be
+able to manage your catalog directly from Odoo:
+
+* Create/modify attributtes and values in Odoo and push then in PrestaShop.
+* Create/modify products and push them in PrestaShop.
+* Create/modify products variants and push them in PrestaShop (combinations).
+* Create/modify category and push them in PrestaShop.
+* Create/modify image and push then in PrestaShop.
+
+Usage
+=====
+
+.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
+ :alt: Try me on Runbot
+ :target: https://runbot.odoo-community.org/runbot/108/8.0
+
+
+Known issues / Roadmap
+======================
+
+* Tests.
+
+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 smashing it by providing a detailed and welcomed feedback.
+
+Credits
+=======
+
+Images
+------
+
+* `PrestaShop logo `_.
+* `Odoo logo `_.
+* `Cable `_.
+
+Contributors
+------------
+
+* Sébastien Beau
+* Benoît Guillot
+* Mikel Arregi
+* Sergio Teruel
+* Pedro M. Baeza
+* Simone Orsi
+* Guillaume Masson
+* Marc Poch
+
+Maintainer
+----------
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+This module is maintained by the OCA.
+
+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.
+
+To contribute to this module, please visit https://odoo-community.org.
diff --git a/connector_prestashop_catalog_manager/__init__.py b/connector_prestashop_catalog_manager/__init__.py
new file mode 100644
index 000000000..7588e52c8
--- /dev/null
+++ b/connector_prestashop_catalog_manager/__init__.py
@@ -0,0 +1,4 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import models
+from . import wizards
diff --git a/connector_prestashop_catalog_manager/__manifest__.py b/connector_prestashop_catalog_manager/__manifest__.py
new file mode 100644
index 000000000..d87c7a59e
--- /dev/null
+++ b/connector_prestashop_catalog_manager/__manifest__.py
@@ -0,0 +1,37 @@
+# Copyright 2011-2013 Camptocamp
+# Copyright 2011-2013 Akretion
+# Copyright 2015 AvanzOSC
+# Copyright 2015-2016 Tecnativa
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Prestashop-Odoo Catalog Manager",
+ "version": "14.0.1.0.0",
+ "license": "AGPL-3",
+ "depends": [
+ "connector_prestashop",
+ "product_categ_image",
+ "product_multi_image",
+ "product_brand",
+ ],
+ "author": "Akretion,"
+ "AvanzOSC,"
+ "Tecnativa,"
+ "Camptocamp SA,"
+ "Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/connector-prestashop",
+ "category": "Connector",
+ "data": [
+ "views/product_attribute_view.xml",
+ "views/product_view.xml",
+ "wizards/export_category_view.xml",
+ "wizards/export_multiple_products_view.xml",
+ "wizards/sync_products_view.xml",
+ "wizards/active_deactive_products_view.xml",
+ "wizards/export_brand_view.xml",
+ "views/product_image_view.xml",
+ "views/product_category_view.xml",
+ "security/ir.model.access.csv",
+ ],
+ "installable": True,
+}
diff --git a/connector_prestashop_catalog_manager/models/__init__.py b/connector_prestashop_catalog_manager/models/__init__.py
new file mode 100644
index 000000000..c04cebf35
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/__init__.py
@@ -0,0 +1,10 @@
+# © 2016 Sergio Teruel
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from . import binding
+from . import product_brand
+from . import product_category
+from . import product_product
+from . import product_template
+from . import product_image
+from . import ir_translation
diff --git a/connector_prestashop_catalog_manager/models/binding/__init__.py b/connector_prestashop_catalog_manager/models/binding/__init__.py
new file mode 100644
index 000000000..c7244de94
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/binding/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2021 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import common
diff --git a/connector_prestashop_catalog_manager/models/binding/common.py b/connector_prestashop_catalog_manager/models/binding/common.py
new file mode 100644
index 000000000..355207005
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/binding/common.py
@@ -0,0 +1,16 @@
+# Copyright 2021 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, models
+
+
+class PrestashopBinding(models.AbstractModel):
+ _inherit = "prestashop.binding"
+
+ @api.model
+ def create(self, vals):
+ ctx = self.env.context.copy()
+ ctx["catalog_manager_ignore_translation"] = True
+ res = super(PrestashopBinding, self.with_context(ctx)).create(vals)
+
+ return res
diff --git a/connector_prestashop_catalog_manager/models/ir_translation.py b/connector_prestashop_catalog_manager/models/ir_translation.py
new file mode 100644
index 000000000..5230ab174
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/ir_translation.py
@@ -0,0 +1,46 @@
+# Copyright 2019 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, models
+
+
+class IrTranslation(models.Model):
+ _inherit = "ir.translation"
+
+ def write_on_source_model(self):
+ """Force a write on source model to make catalog_manager
+ export the translation
+ """
+ for translation in self:
+ if translation.type == "model":
+ # get model and ir_field
+ model, fieldname = translation.name.split(",")
+ model_obj = translation.env[model]
+ instance = model_obj.browse(translation.res_id)
+ instance_vals = instance.read([fieldname])[0]
+ untranslated_content = instance_vals[fieldname]
+ instance.with_context(catalog_manager_force_translation=True).write(
+ {fieldname: untranslated_content}
+ )
+ return True
+
+ def write(self, vals):
+ res = False
+ for translation in self:
+ if translation.env.context.get("catalog_manager_force_translation", False):
+ continue
+ res = super().write(vals)
+
+ self.write_on_source_model()
+ return res
+
+ @api.model
+ def create(self, vals):
+ res = super().create(vals)
+
+ if not self.env.context.get("catalog_manager_ignore_translation", False):
+ # It is called from a binding creation so,
+ # Once the binding will be created, it will export everything.
+ # this way we avoid job duplicities exporting the same instance
+ res.write_on_source_model()
+ return res
diff --git a/connector_prestashop_catalog_manager/models/product_brand/__init__.py b/connector_prestashop_catalog_manager/models/product_brand/__init__.py
new file mode 100644
index 000000000..c8f1c4733
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_brand/__init__.py
@@ -0,0 +1,6 @@
+# Copyright 2020 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import common
+from . import exporter
+from . import deleter
diff --git a/connector_prestashop_catalog_manager/models/product_brand/common.py b/connector_prestashop_catalog_manager/models/product_brand/common.py
new file mode 100644
index 000000000..95550895e
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_brand/common.py
@@ -0,0 +1,49 @@
+# Copyright 2020 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+
+
+class PrestashopProductBrandListener(Component):
+ _name = "prestashop.product.brand.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "prestashop.product.brand"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_create(self, record, fields=None):
+ """Called when a record is created"""
+ record.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ @skip_if(lambda self, record, **kwargs: self.need_to_export(record, **kwargs))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ record.with_delay().export_record(fields=fields)
+
+
+class ProductBrandListener(Component):
+ _name = "product.brand.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "product.brand"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.brand"
+ )
+ prestashop_id = binder.to_external(binding)
+ if prestashop_id:
+ self.env["prestashop.product.brand"].with_delay().export_delete_record(
+ binding.backend_id, prestashop_id
+ )
diff --git a/connector_prestashop_catalog_manager/models/product_brand/deleter.py b/connector_prestashop_catalog_manager/models/product_brand/deleter.py
new file mode 100644
index 000000000..52fade7d1
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_brand/deleter.py
@@ -0,0 +1,12 @@
+# Copyright 2020 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+
+
+class ProductBrandDeleter(Component):
+ _name = "prestashop.product.brand.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = [
+ "prestashop.product.brand",
+ ]
diff --git a/connector_prestashop_catalog_manager/models/product_brand/exporter.py b/connector_prestashop_catalog_manager/models/product_brand/exporter.py
new file mode 100644
index 000000000..c0bb2e4bf
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_brand/exporter.py
@@ -0,0 +1,25 @@
+# Copyright 2020 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import mapping
+
+
+class ProductBrandExporter(Component):
+ _name = "prestashop.product.brand.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.brand"
+
+
+class ProductBrandExportMapper(Component):
+ _name = "prestashop.product.brand.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.brand"
+
+ direct = [
+ ("name", "name"),
+ ]
+
+ @mapping
+ def active(self, record):
+ return {"active": 1}
diff --git a/connector_prestashop_catalog_manager/models/product_category/__init__.py b/connector_prestashop_catalog_manager/models/product_category/__init__.py
new file mode 100644
index 000000000..2c2fc9544
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_category/__init__.py
@@ -0,0 +1,3 @@
+from . import common
+from . import exporter
+from . import deleter
diff --git a/connector_prestashop_catalog_manager/models/product_category/common.py b/connector_prestashop_catalog_manager/models/product_category/common.py
new file mode 100644
index 000000000..cfeacef54
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_category/common.py
@@ -0,0 +1,141 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo import fields, models
+from odoo.tools import config
+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+from odoo.addons.connector_prestashop.components.backend_adapter import (
+ PrestaShopWebServiceImage,
+)
+
+
+class ProductCategory(models.Model):
+ _inherit = "product.category"
+
+ prestashop_image_bind_ids = fields.One2many(
+ comodel_name="prestashop.categ.image",
+ inverse_name="odoo_id",
+ copy=False,
+ string="PrestaShop Image Bindings",
+ )
+
+
+class PrestashopCategImage(models.Model):
+ _name = "prestashop.categ.image"
+ _inherit = "prestashop.binding.odoo"
+ _inherits = {"product.category": "odoo_id"}
+ _description = "Prestashop Category Image"
+
+ odoo_id = fields.Many2one(
+ comodel_name="product.category",
+ string="Product",
+ required=True,
+ ondelete="cascade",
+ )
+
+
+class PrestashopCategImageModelBinder(Component):
+ _name = "prestashop.categ.image.binder"
+ _inherit = "prestashop.binder"
+ _apply_on = "prestashop.categ.image"
+
+
+class CategImageAdapter(Component):
+ _name = "prestashop.categ.image.adapter"
+ _inherit = "prestashop.crud.adapter"
+ _apply_on = "prestashop.categ.image"
+ _prestashop_image_model = "categories"
+
+ def connect(self):
+ debug = False
+ if config["log_level"] == "debug":
+ debug = True
+ return PrestaShopWebServiceImage(
+ self.prestashop.api_url, self.prestashop.webservice_key, debug=debug
+ )
+
+ def read(self, category_id, image_id, options=None):
+ # pylint: disable=method-required-super
+ api = self.connect()
+ return api.get_image(
+ self._prestashop_image_model, category_id, image_id, options=options
+ )
+
+ def create(self, attributes=None):
+ # pylint: disable=method-required-super
+ api = self.connect()
+ image_binary = attributes["image"]
+ img_filename = attributes["name"]
+ image_url = "images/{}/{}".format(
+ self._prestashop_image_model,
+ str(attributes["categ_id"]),
+ )
+ return api.add(image_url, files=[("image", img_filename, image_binary)])
+
+ def write(self, id_, attributes=None):
+ # pylint: disable=method-required-super
+ api = self.connect()
+ image_binary = attributes["image"]
+ img_filename = attributes["name"]
+ delete_url = "images/%s" % (self._prestashop_image_model)
+ api.delete(delete_url, str(attributes["categ_id"]))
+ image_url = "images/{}/{}".format(
+ self._prestashop_image_model,
+ str(attributes["categ_id"]),
+ )
+ return api.add(image_url, files=[("image", img_filename, image_binary)])
+
+
+class PrestashopProductCategoryListener(Component):
+ _name = "prestashop.product.category.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "prestashop.product.category"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_create(self, record, fields=None):
+ """Called when a record is created"""
+ record.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ @skip_if(lambda self, record, **kwargs: self.need_to_export(record, **kwargs))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ record.with_delay().export_record(fields=fields)
+
+
+class ProductCategoryListener(Component):
+ _name = "product.category.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "product.category"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay().export_record(fields=fields)
+ if "image" in fields:
+ if record.prestashop_image_bind_ids:
+ for image in record.prestashop_image_bind_ids:
+ image.with_delay().export_record(fields=fields)
+ else:
+ for presta_categ in record.prestashop_bind_ids:
+ image = self.env["prestashop.categ.image"].create(
+ {"backend_id": presta_categ.backend_id.id, "odoo_id": record.id}
+ )
+ image.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.category"
+ )
+ prestashop_id = binder.to_external(binding)
+ if prestashop_id:
+ self.env[
+ "prestashop.product.category"
+ ].with_delay().export_delete_record(binding.backend_id, prestashop_id)
diff --git a/connector_prestashop_catalog_manager/models/product_category/deleter.py b/connector_prestashop_catalog_manager/models/product_category/deleter.py
new file mode 100644
index 000000000..4d8211ee0
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_category/deleter.py
@@ -0,0 +1,13 @@
+# Copyright 2018 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+#
+
+from odoo.addons.component.core import Component
+
+
+class ProductCategoryDeleter(Component):
+ _name = "prestashop.product.category.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = [
+ "prestashop.product.category",
+ ]
diff --git a/connector_prestashop_catalog_manager/models/product_category/exporter.py b/connector_prestashop_catalog_manager/models/product_category/exporter.py
new file mode 100644
index 000000000..08c22a9fa
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_category/exporter.py
@@ -0,0 +1,99 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import changed_by, mapping
+
+from ..product_template.exporter import get_slug
+
+
+class ProductCategoryExporter(Component):
+ _name = "prestashop.product.category.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.category"
+
+ def _export_dependencies(self):
+ """Export the dependencies for the category"""
+ category_binder = self.binder_for("prestashop.product.category")
+ categories_obj = self.env["prestashop.product.category"]
+ for category in self.binding:
+ self.export_parent_category(
+ category.odoo_id.parent_id, category_binder, categories_obj
+ )
+
+ def export_parent_category(self, category, binder, ps_categ_obj):
+ if not category:
+ return
+ ext_id = binder.to_external(category.id, wrap=True)
+ if ext_id:
+ return ext_id
+ res = {
+ "backend_id": self.backend_record.id,
+ "odoo_id": category.id,
+ "link_rewrite": get_slug(category.name),
+ }
+ category_ext = ps_categ_obj.with_context(connector_no_export=True).create(res)
+ parent_cat_id = category_ext.export_record()
+ return parent_cat_id
+
+
+class ProductCategoryExportMapper(Component):
+ _name = "prestashop.product.category.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.category"
+
+ direct = [
+ ("default_shop_id", "id_shop_default"),
+ ("active", "active"),
+ ("position", "position"),
+ ]
+ # handled by base mapping `translatable_fields`
+ _translatable_fields = [
+ ("name", "name"),
+ ("link_rewrite", "link_rewrite"),
+ ("description", "description"),
+ ("meta_description", "meta_description"),
+ ("meta_keywords", "meta_keywords"),
+ ("meta_title", "meta_title"),
+ ]
+
+ @changed_by("parent_id")
+ @mapping
+ def parent_id(self, record):
+ if not record["parent_id"]:
+ return {"id_parent": 2}
+ category_binder = self.binder_for("prestashop.product.category")
+ ext_categ_id = category_binder.to_external(record.parent_id.id, wrap=True)
+ return {"id_parent": ext_categ_id}
+
+
+class CategImageExporter(Component):
+ _name = "prestashop.product.category.image.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.categ.image"
+
+ def _create(self, data):
+ """Create the Prestashop record"""
+ if self.backend_adapter.create(data):
+ return 1
+
+ def _update(self, data):
+ return 1
+
+
+class CategImageExportMapper(Component):
+ _name = "prestashop.product.category.image.mapper"
+ _inherit = "prestashop.export.mapper"
+ _apply_on = "prestashop.categ.image"
+
+ @changed_by("image")
+ @mapping
+ def image(self, record):
+ name = record.name.lower() + ".jpg"
+ return {"image": record["image"], "name": name}
+
+ @changed_by("odoo_id")
+ @mapping
+ def odoo_id(self, record):
+ binder = self.binder_for("prestashop.product.category")
+ ext_categ_id = binder.to_external(record.odoo_id.id, wrap=True)
+ return {"categ_id": ext_categ_id}
diff --git a/connector_prestashop_catalog_manager/models/product_image/__init__.py b/connector_prestashop_catalog_manager/models/product_image/__init__.py
new file mode 100644
index 000000000..2c2fc9544
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_image/__init__.py
@@ -0,0 +1,3 @@
+from . import common
+from . import exporter
+from . import deleter
diff --git a/connector_prestashop_catalog_manager/models/product_image/common.py b/connector_prestashop_catalog_manager/models/product_image/common.py
new file mode 100644
index 000000000..b5744d443
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_image/common.py
@@ -0,0 +1,51 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+
+
+class ProductImage(models.Model):
+ _inherit = "base_multi_image.image"
+
+ front_image = fields.Boolean(string="Front image")
+
+
+class PrestashopProductImageListener(Component):
+ _name = "prestashop.product.image.event.listener"
+ _inherit = "base.connector.listener"
+ _apply_on = "base_multi_image.image"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ binding.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ product = self.env[record.owner_model].browse(record.owner_id)
+ if product.exists():
+ template = product.prestashop_bind_ids.filtered(
+ lambda x: x.backend_id == binding.backend_id
+ )
+ if not template:
+ return
+
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.image"
+ )
+ prestashop_id = binder.to_external(binding)
+ attributes = {
+ "id_product": template.prestashop_id,
+ }
+ if prestashop_id:
+ self.env[
+ "prestashop.product.image"
+ ].with_delay().export_delete_record(
+ binding.backend_id, prestashop_id, attributes
+ )
diff --git a/connector_prestashop_catalog_manager/models/product_image/deleter.py b/connector_prestashop_catalog_manager/models/product_image/deleter.py
new file mode 100644
index 000000000..0449a19e0
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_image/deleter.py
@@ -0,0 +1,10 @@
+# Copyright 2018 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+
+
+class ProductImageDeleter(Component):
+ _name = "prestashop.product.image.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = "prestashop.product.image"
diff --git a/connector_prestashop_catalog_manager/models/product_image/exporter.py b/connector_prestashop_catalog_manager/models/product_image/exporter.py
new file mode 100644
index 000000000..c0a5cb55a
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_image/exporter.py
@@ -0,0 +1,162 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import os
+import os.path
+
+from odoo.tools.translate import _
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import mapping
+from odoo.addons.connector_prestashop.components.backend_adapter import (
+ PrestaShopWebServiceImage,
+)
+
+
+class ProductImageExporter(Component):
+ _name = "prestashop.product.image.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.image"
+
+ def _run(self, fields=None):
+ """Flow of the synchronization, implemented in inherited classes"""
+ assert self.binding_id
+ assert self.binding
+
+ if self._has_to_skip():
+ return
+
+ # export the missing linked resources
+ self._export_dependencies()
+ map_record = self.mapper.map_record(self.binding)
+
+ if self.prestashop_id:
+ record = list(map_record.values())
+ if not record:
+ return _("Nothing to export.")
+ # special check on data before export
+ self._validate_data(record)
+ exported_vals = self._update(record)
+ else:
+ record = map_record.values(for_create=True)
+ if not record:
+ return _("Nothing to export.")
+ # special check on data before export
+ self._validate_data(record)
+ exported_vals = self._create(record)
+ self._after_export()
+ if (
+ exported_vals
+ and exported_vals.get("prestashop")
+ and exported_vals["prestashop"].get("image")
+ ):
+ self.prestashop_id = int(exported_vals["prestashop"]["image"].get("id"))
+
+ self._link_image_to_url()
+ message = _("Record exported with ID %s on Prestashop.")
+ return message % self.prestashop_id
+
+ def _link_image_to_url(self):
+ """Change image storage to a url linked to product prestashop image"""
+ api = PrestaShopWebServiceImage(
+ api_url=self.backend_record.location,
+ api_key=self.backend_record.webservice_key,
+ )
+ full_public_url = api.get_image_public_url(
+ {
+ "id_image": str(self.prestashop_id),
+ "type": "image/jpeg",
+ }
+ )
+ if self.binding.url != full_public_url:
+ self.binding.with_context(connector_no_export=True).write(
+ {
+ "url": full_public_url,
+ "file_db_store": False,
+ "storage": "url",
+ }
+ )
+
+
+class ProductImageExportMapper(Component):
+ _name = "prestashop.product.image.mapper"
+ _inherit = "prestashop.export.mapper"
+ _apply_on = "prestashop.product.image"
+
+ direct = [
+ ("name", "name"),
+ ]
+
+ def _get_file_name(self, record):
+ """
+ Get file name with extension from depending storage.
+ :param record: browse record
+ :return: string: file name.extension.
+ """
+ file_name = record.odoo_id.filename
+ if not file_name:
+ storage = record.odoo_id.storage
+ if storage == "url":
+ file_name = os.path.splitext(os.path.basename(record.odoo_id.url))
+ elif storage == "db":
+ if not record.odoo_id.filename:
+ file_name = "{}_{}.jpg".format(
+ record.odoo_id.owner_model,
+ record.odoo_id.owner_id,
+ )
+ file_name = os.path.splitext(
+ os.path.basename(record.odoo_id.filename or file_name)
+ )
+ elif storage == "file":
+ file_name = os.path.splitext(os.path.basename(record.odoo_id.path))
+ elif storage == "filestore":
+ mimetype = record.odoo_id.attachment_id.mimetype
+ if "/" in mimetype:
+ ext = mimetype.split("/")[-1]
+ else:
+ ext = mimetype
+ if ext == "jpeg":
+ ext = "jpg"
+ file_name = [record.odoo_id.attachment_id.res_name, ext]
+ return file_name
+
+ @mapping
+ def source_image(self, record):
+ content = getattr(
+ record.odoo_id, "_get_image_from_%s" % record.odoo_id.storage
+ )()
+ return {"content": content}
+
+ @mapping
+ def product_id(self, record):
+ if record.odoo_id.owner_model == "product.product":
+ product_tmpl = (
+ record.env["product.product"]
+ .browse(record.odoo_id.owner_id)
+ .product_tmpl_id
+ )
+ else:
+ product_tmpl = record.env["product.template"].browse(
+ record.odoo_id.owner_id
+ )
+ binder = self.binder_for("prestashop.product.template")
+ ps_product_id = binder.to_external(product_tmpl, wrap=True)
+ return {"id_product": ps_product_id}
+
+ @mapping
+ def extension(self, record):
+ return {"extension": self._get_file_name(record)[1]}
+
+ @mapping
+ def legend(self, record):
+ return {"legend": record.name}
+
+ @mapping
+ def filename(self, record):
+ file_name = record.filename
+ if not file_name:
+ name_tuple = self._get_file_name(record)
+ if name_tuple[1].startswith("."):
+ file_name = "".join(name_tuple)
+ else:
+ file_name = ".".join(name_tuple)
+ return {"filename": file_name}
diff --git a/connector_prestashop_catalog_manager/models/product_product/__init__.py b/connector_prestashop_catalog_manager/models/product_product/__init__.py
new file mode 100644
index 000000000..bc9719d7f
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_product/__init__.py
@@ -0,0 +1,3 @@
+from . import exporter
+from . import common
+from . import deleter
diff --git a/connector_prestashop_catalog_manager/models/product_product/common.py b/connector_prestashop_catalog_manager/models/product_product/common.py
new file mode 100644
index 000000000..cb86f20a1
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_product/common.py
@@ -0,0 +1,160 @@
+# © 2016 Sergio Teruel
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import fields, models
+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+from odoo.addons.connector_prestashop.models.product_template.common import (
+ PrestashopProductQuantityListener,
+)
+
+
+class PrestashopProductCombination(models.Model):
+ _inherit = "prestashop.product.combination"
+ minimal_quantity = fields.Integer(
+ string="Minimal Quantity",
+ default=1,
+ help="Minimal Sale quantity",
+ )
+
+
+class PrestashopProductProductListener(Component):
+ _name = "prestashop.product.product.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "prestashop.product.combination"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_create(self, record, fields=None):
+ """Called when a record is created"""
+ record.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ @skip_if(lambda self, record, **kwargs: self.need_to_export(record, **kwargs))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ inventory_fields = PrestashopProductQuantityListener._get_inventory_fields(self)
+ fields = list(set(fields).difference(set(inventory_fields)))
+ if fields:
+ record.with_delay().export_record(fields=fields)
+
+
+class ProductProductListener(Component):
+ _name = "product.product.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "product.product"
+
+ EXCLUDE_FIELDS = ["list_price"]
+
+ def prestashop_product_combination_unlink(self, record):
+ # binding is deactivate when deactive a product variant
+ for binding in record.prestashop_combinations_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.combination"
+ )
+ prestashop_id = binder.to_external(binding)
+ binding.with_delay().export_delete_record(binding.backend_id, prestashop_id)
+ record.prestashop_combinations_bind_ids.unlink()
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for field in self.EXCLUDE_FIELDS:
+ if field in fields:
+ fields.remove(field)
+ if "active" in fields and not record["active"]:
+ self.prestashop_product_combination_unlink(record)
+ return
+ if fields:
+ priority = 20
+ if "default_on" in fields and record["default_on"]:
+ # PS has to uncheck actual default combination first
+ priority = 99
+ for binding in record.prestashop_combinations_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay(priority=priority).export_record(fields=fields)
+
+ def on_product_price_changed(self, record):
+ fields = ["standard_price", "impact_price", "lst_price", "list_price"]
+ for binding in record.prestashop_combinations_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay(priority=20).export_record(fields=fields)
+
+
+class PrestashopAttributeListener(Component):
+ _name = "prestashop.attribute.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = [
+ "prestashop.product.combination.option",
+ "prestashop.product.combination.option.value",
+ ]
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_create(self, record, fields=None):
+ """Called when a record is created"""
+ record.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ @skip_if(lambda self, record, **kwargs: self.need_to_export(record, **kwargs))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ record.with_delay().export_record(fields=fields)
+
+
+class AttributeListener(Component):
+ _name = "attribute.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = [
+ "product.attribute",
+ ]
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.combination.option"
+ )
+ prestashop_id = binder.to_external(binding)
+ if prestashop_id:
+ self.env[
+ "prestashop.product.combination.option"
+ ].with_delay().export_delete_record(binding.backend_id, prestashop_id)
+
+
+class AttributeValueListener(Component):
+ _name = "attribute.value.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = [
+ "product.attribute.value",
+ ]
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ if not self.need_to_export(binding, fields):
+ binding.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.combination.option.value"
+ )
+ prestashop_id = binder.to_external(binding)
+ if prestashop_id:
+ self.env[
+ "prestashop.product.combination.option.value"
+ ].with_delay().export_delete_record(binding.backend_id, prestashop_id)
diff --git a/connector_prestashop_catalog_manager/models/product_product/deleter.py b/connector_prestashop_catalog_manager/models/product_product/deleter.py
new file mode 100644
index 000000000..148a29d6c
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_product/deleter.py
@@ -0,0 +1,19 @@
+# Copyright 2018 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.addons.component.core import Component
+
+
+class ProductCombinationDeleter(Component):
+ _name = "prestashop.product.combination.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = "prestashop.product.combination"
+
+
+class ProductCombinationOptionDeleter(Component):
+ _name = "prestashop.product.combination.option.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = [
+ "prestashop.product.combination.option",
+ "prestashop.product.combination.option.value",
+ ]
diff --git a/connector_prestashop_catalog_manager/models/product_product/exporter.py b/connector_prestashop_catalog_manager/models/product_product/exporter.py
new file mode 100644
index 000000000..1d6ce4acd
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_product/exporter.py
@@ -0,0 +1,293 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import changed_by, mapping
+
+_logger = logging.getLogger(__name__)
+
+
+class ProductCombinationExporter(Component):
+ _name = "prestashop.product.combination.exporter"
+ _inherit = "translation.prestashop.exporter"
+ _apply_on = "prestashop.product.combination"
+
+ def _create(self, record):
+ """
+ :param record: browse record to create in prestashop
+ :return integer: Prestashop record id
+ """
+ res = super()._create(record)
+ return res["prestashop"]["combination"]["id"]
+
+ def _export_images(self):
+ if self.binding.image_ids:
+ image_binder = self.binder_for("prestashop.product.image")
+ for image_line in self.binding.image_ids:
+ image_ext_id = image_binder.to_external(image_line.id, wrap=True)
+ if not image_ext_id:
+ image_ext = (
+ self.env["prestashop.product.image"]
+ .with_context(connector_no_export=True)
+ .create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": image_line.id,
+ }
+ )
+ .id
+ )
+ image_content = getattr(
+ image_line, "_get_image_from_%s" % image_line.storage
+ )()
+ image_ext.export_record(image_content)
+
+ def _export_dependencies(self):
+ """Export the dependencies for the product"""
+ # TODO add export of category
+ attribute_binder = self.binder_for("prestashop.product.combination.option")
+ option_binder = self.binder_for("prestashop.product.combination.option.value")
+ Option = self.env["prestashop.product.combination.option"]
+ OptionValue = self.env["prestashop.product.combination.option.value"]
+ for value in self.binding.product_template_attribute_value_ids:
+ prestashop_option_id = attribute_binder.to_external(
+ value.attribute_id.id, wrap=True
+ )
+ if not prestashop_option_id:
+ option_binding = Option.search(
+ [
+ ("backend_id", "=", self.backend_record.id),
+ ("odoo_id", "=", value.attribute_id.id),
+ ]
+ )
+ if not option_binding:
+ option_binding = Option.with_context(
+ connector_no_export=True
+ ).create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": value.attribute_id.id,
+ }
+ )
+ option_binding.export_record()
+ prestashop_value_id = option_binder.to_external(
+ value.product_attribute_value_id.id, wrap=True
+ )
+ if not prestashop_value_id:
+ value_binding = OptionValue.search(
+ [
+ ("backend_id", "=", self.backend_record.id),
+ ("odoo_id", "=", value.id),
+ ]
+ )
+ if not value_binding:
+ option_binding = Option.search(
+ [
+ ("backend_id", "=", self.backend_record.id),
+ ("odoo_id", "=", value.attribute_id.id),
+ ]
+ )
+ value_binding = OptionValue.with_context(
+ connector_no_export=True
+ ).create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": value.product_attribute_value_id.id,
+ "id_attribute_group": option_binding.id,
+ }
+ )
+ value_binding.export_record()
+ # self._export_images()
+
+ def update_quantities(self):
+ self.binding.odoo_id.with_context(self.env.context).update_prestashop_qty()
+
+ def _after_export(self):
+ self.update_quantities()
+
+
+class ProductCombinationExportMapper(Component):
+ _name = "prestashop.product.combination.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.combination"
+
+ direct = [
+ ("default_code", "reference"),
+ ("active", "active"),
+ ("barcode", "ean13"),
+ ("minimal_quantity", "minimal_quantity"),
+ ("weight", "weight"),
+ ]
+
+ def _get_factor_tax(self, tax):
+ factor_tax = tax.price_include and (1 + tax.amount / 100) or 1.0
+ return factor_tax
+
+ @mapping
+ def combination_default(self, record):
+ return {"default_on": int(record["default_on"])}
+
+ def get_main_template_id(self, record):
+ template_binder = self.binder_for("prestashop.product.template")
+ return template_binder.to_external(record.main_template_id.id)
+
+ @mapping
+ def main_template_id(self, record):
+ return {"id_product": self.get_main_template_id(record)}
+
+ @changed_by("impact_price")
+ @mapping
+ def _unit_price_impact(self, record):
+ pricelist = record.backend_id.pricelist_id
+ if pricelist:
+ tmpl_prices = pricelist.get_products_price(
+ [record.odoo_id.product_tmpl_id], [1.0], [None]
+ )
+ tmpl_price = tmpl_prices.get(record.odoo_id.product_tmpl_id.id)
+ product_prices = pricelist.get_products_price(
+ [record.odoo_id], [1.0], [None]
+ )
+ product_price = product_prices.get(record.odoo_id.id)
+ extra_to_export = product_price - tmpl_price
+ else:
+ extra_to_export = record.impact_price
+ tax = record.taxes_id[:1]
+ if tax.price_include and tax.amount_type == "percent":
+ # 6 is the rounding precision used by PrestaShop for the
+ # tax excluded price. we can get back a 2 digits tax included
+ # price from the 6 digits rounded value
+ return {"price": round(extra_to_export / self._get_factor_tax(tax), 6)}
+ else:
+ return {"price": extra_to_export}
+
+ @changed_by("standard_price")
+ @mapping
+ def cost_price(self, record):
+ wholesale_price = float("{:.2f}".format(record.standard_price))
+ return {"wholesale_price": wholesale_price}
+
+ def _get_product_option_value(self, record):
+ option_value = []
+ option_binder = self.binder_for("prestashop.product.combination.option.value")
+ for value in record.product_template_attribute_value_ids:
+ value_ext_id = option_binder.to_external(
+ value.product_attribute_value_id.id, wrap=True
+ )
+ if value_ext_id:
+ option_value.append({"id": value_ext_id})
+ return option_value
+
+ def _get_combination_image(self, record):
+ images = []
+ image_binder = self.binder_for("prestashop.product.image")
+ for image in record.image_ids:
+ image_ext_id = image_binder.to_external(image.id, wrap=True)
+ if image_ext_id:
+ images.append({"id": image_ext_id})
+ return images
+
+ @changed_by("product_template_attribute_value_ids", "image_ids")
+ @mapping
+ def associations(self, record):
+ return {
+ "associations": {
+ "product_option_values": {
+ "product_option_value": self._get_product_option_value(record)
+ },
+ "images": {"image": self._get_combination_image(record)},
+ }
+ }
+
+ @mapping
+ def low_stock_alert(self, record):
+ low_stock_alert = False
+ if record.product_tmpl_id.prestashop_bind_ids:
+ for presta_prod_tmpl in record.product_tmpl_id.prestashop_bind_ids:
+ if presta_prod_tmpl.low_stock_alert:
+ low_stock_alert = True
+ break
+ return {"low_stock_alert": "1" if low_stock_alert else "0"}
+
+
+class ProductCombinationOptionExporter(Component):
+ _name = "prestashop.product.combination.option.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.combination.option"
+
+ def _create(self, record):
+ res = super()._create(record)
+ return res["prestashop"]["product_option"]["id"]
+
+
+class ProductCombinationOptionExportMapper(Component):
+ _name = "prestashop.product.combination.option.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.combination.option"
+
+ direct = [
+ ("prestashop_position", "position"),
+ ("display_type", "group_type"),
+ ]
+
+ _translatable_fields = [
+ ("name", "name"),
+ ("name", "public_name"),
+ ]
+
+
+class ProductCombinationOptionValueExporter(Component):
+ _name = "prestashop.product.combination.option.value.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.combination.option.value"
+
+ def _create(self, record):
+ res = super()._create(record)
+ return res["prestashop"]["product_option_value"]["id"]
+
+ def _export_dependencies(self):
+ """Export the dependencies for the record"""
+ attribute_id = self.binding.attribute_id.id
+ # export product attribute
+ attr_model = "prestashop.product.combination.option"
+ binder = self.binder_for(attr_model)
+ if not binder.to_external(attribute_id, wrap=True):
+ with self.backend_id.work_on(attr_model) as work:
+ exporter = work.component(usage="record.exporter")
+ exporter.run(attribute_id)
+ return
+
+
+class ProductCombinationOptionValueExportMapper(Component):
+ _name = "prestashop.product.combination.option.value.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.combination.option.value"
+
+ direct = [
+ ("name", "value"),
+ ("prestashop_position", "position"),
+ ]
+ # handled by base mapping `translatable_fields`
+ _translatable_fields = [
+ ("name", "name"),
+ ]
+
+ @mapping
+ def prestashop_product_attribute_id(self, record):
+ attribute_binder = self.binder_for(
+ "prestashop.product.combination.option.value"
+ )
+ return {
+ "id_feature": attribute_binder.to_external(
+ record.attribute_id.id, wrap=True
+ )
+ }
+
+ @mapping
+ def prestashop_product_group_attribute_id(self, record):
+ attribute_binder = self.binder_for("prestashop.product.combination.option")
+ return {
+ "id_attribute_group": attribute_binder.to_external(
+ record.attribute_id.id, wrap=True
+ ),
+ }
diff --git a/connector_prestashop_catalog_manager/models/product_template/__init__.py b/connector_prestashop_catalog_manager/models/product_template/__init__.py
new file mode 100644
index 000000000..2c2fc9544
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_template/__init__.py
@@ -0,0 +1,3 @@
+from . import common
+from . import exporter
+from . import deleter
diff --git a/connector_prestashop_catalog_manager/models/product_template/common.py b/connector_prestashop_catalog_manager/models/product_template/common.py
new file mode 100644
index 000000000..e5b8017c5
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_template/common.py
@@ -0,0 +1,125 @@
+# © 2016 Sergio Teruel
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import fields, models
+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+
+
+class PrestashopProductTemplate(models.Model):
+ _inherit = "prestashop.product.template"
+
+ meta_title = fields.Char(string="Meta Title", translate=True)
+ meta_description = fields.Char(string="Meta Description", translate=True)
+ meta_keywords = fields.Char(string="Meta Keywords", translate=True)
+ tags = fields.Char(string="Tags", translate=True)
+ online_only = fields.Boolean(string="Online Only")
+ additional_shipping_cost = fields.Float(
+ string="Additional Shipping Price",
+ digits="Product Price",
+ help="Additionnal Shipping Price for the product on Prestashop",
+ )
+ available_now = fields.Char(string="Available Now", translate=True)
+ available_later = fields.Char(string="Available Later", translate=True)
+ available_date = fields.Date(string="Available Date")
+ minimal_quantity = fields.Integer(
+ string="Minimal Quantity",
+ help="Minimal Sale quantity",
+ default=1,
+ )
+ state = fields.Boolean(string="State", default=True)
+ visibility = fields.Char(translate=True)
+ low_stock_threshold = fields.Integer(
+ string="Low Stock Threshold",
+ help="Low Stock Threshold",
+ default=0,
+ )
+ low_stock_alert = fields.Integer(
+ string="Low Stock Alert",
+ help="Low Stock Alert",
+ default=0,
+ )
+
+
+class PrestashopProductTemplateListener(Component):
+ _name = "prestashop.product.template.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "prestashop.product.template"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_create(self, record, fields=None):
+ """Called when a record is created"""
+ record.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ @skip_if(lambda self, record, **kwargs: self.need_to_export(record, **kwargs))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ record.with_delay().export_record(fields=fields)
+ if "minimal_quantity" in fields:
+ record.product_variant_ids.mapped(
+ "prestashop_combinations_bind_ids"
+ ).filtered(lambda cb: cb.backend_id == record.backend_id).write(
+ {"minimal_quantity": record.minimal_quantity}
+ )
+
+
+class ProductTemplateListener(Component):
+ _name = "product.template.event.listener"
+ _inherit = "prestashop.connector.listener"
+ _apply_on = "product.template"
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_write(self, record, fields=None):
+ """Called when a record is written"""
+ for binding in record.prestashop_bind_ids:
+ # when assigning a product.attribute to a product.template,
+ # write is called 2 times.
+ # To avoid duplicates entries on Prestashop, ignore the empty write
+ if fields and not self.need_to_export(binding, fields):
+ binding.with_delay().export_record(fields=fields)
+
+ @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+ def on_record_unlink(self, record, fields=None):
+ """Called when a record is deleted"""
+ for binding in record.prestashop_bind_ids:
+ work = self.work.work_on(collection=binding.backend_id)
+ binder = work.component(
+ usage="binder", model_name="prestashop.product.template"
+ )
+ prestashop_id = binder.to_external(binding)
+ if prestashop_id:
+ self.env[
+ "prestashop.product.template"
+ ].with_delay().export_delete_record(binding.backend_id, prestashop_id)
+
+
+class TemplateAdapter(Component):
+ _inherit = "prestashop.product.template.adapter"
+
+ def write(self, id_, attributes=None):
+ # Prestashop wants all product data:
+ prestashop_data = self.client.get(self._prestashop_model, id_)
+
+ # Remove read-only fields:
+ prestashop_data["product"].pop("manufacturer_name", False)
+ prestashop_data["product"].pop("quantity", False)
+
+ # Remove position_in_category to avoid these PrestaShop issues:
+ # https://github.com/PrestaShop/PrestaShop/issues/14903
+ # https://github.com/PrestaShop/PrestaShop/issues/15380
+ prestashop_data["product"].pop("position_in_category", False)
+
+ full_attributes = prestashop_data["product"].copy()
+ fa_assoc = full_attributes["associations"]
+ for field in attributes:
+ if field != "associations":
+ full_attributes[field] = attributes[field]
+ continue
+ for association, value in attributes["associations"].items():
+ fa_assoc[association] = value
+
+ res = super().write(id_, full_attributes)
+
+ return res
diff --git a/connector_prestashop_catalog_manager/models/product_template/deleter.py b/connector_prestashop_catalog_manager/models/product_template/deleter.py
new file mode 100644
index 000000000..f5201652b
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_template/deleter.py
@@ -0,0 +1,13 @@
+# Copyright 2018 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+#
+
+from odoo.addons.component.core import Component
+
+
+class ProductCombinationOptionDeleter(Component):
+ _name = "prestashop.product.template.deleter"
+ _inherit = "prestashop.deleter"
+ _apply_on = [
+ "prestashop.product.template",
+ ]
diff --git a/connector_prestashop_catalog_manager/models/product_template/exporter.py b/connector_prestashop_catalog_manager/models/product_template/exporter.py
new file mode 100644
index 000000000..117891afe
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_template/exporter.py
@@ -0,0 +1,334 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import re
+import unicodedata
+from datetime import timedelta
+
+from odoo import fields
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import changed_by, m2o_to_external, mapping
+
+try:
+ import slugify as slugify_lib
+except ImportError:
+ slugify_lib = None
+
+
+def get_slug(name):
+ if slugify_lib:
+ try:
+ return slugify_lib.slugify(name)
+ except TypeError:
+ pass
+ uni = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
+ slug = re.sub(r"[\W_]", " ", uni).strip().lower()
+ slug = re.sub(r"[-\s]+", "-", slug)
+ return slug
+
+
+class ProductTemplateExporter(Component):
+ _name = "prestashop.product.template.exporter"
+ _inherit = "translation.prestashop.exporter"
+ _apply_on = "prestashop.product.template"
+
+ def _create(self, record):
+ res = super()._create(record)
+ self.write_binging_vals(self.binding, record)
+ return res["prestashop"]["product"]["id"]
+
+ def _update(self, data):
+ """Update an Prestashop record"""
+ assert self.prestashop_id
+ self.check_images()
+ self.backend_adapter.write(self.prestashop_id, data)
+
+ def write_binging_vals(self, erp_record, ps_record):
+ keys_to_update = [
+ ("description_short_html", "description_short"),
+ ("description_html", "description"),
+ ]
+ trans = self.component(usage="record.importer")
+ splitted_record = trans._split_per_language(ps_record)
+ for lang_code, prestashop_record in list(splitted_record.items()):
+ vals = {}
+ for key in keys_to_update:
+ vals[key[0]] = prestashop_record[key[1]]
+ erp_record.with_context(connector_no_export=True, lang=lang_code).write(
+ vals
+ )
+
+ def export_categories(self, category):
+ if not category:
+ return
+ category_binder = self.binder_for("prestashop.product.category")
+ ext_id = category_binder.to_external(category, wrap=True)
+ if ext_id:
+ return ext_id
+
+ ps_categ_obj = self.env["prestashop.product.category"]
+ position_cat_id = ps_categ_obj.search([], order="position desc", limit=1)
+ obj_position = position_cat_id.position + 1
+ res = {
+ "backend_id": self.backend_record.id,
+ "odoo_id": category.id,
+ "link_rewrite": get_slug(category.name),
+ "position": obj_position,
+ }
+ binding = ps_categ_obj.with_context(connector_no_export=True).create(res)
+ binding.export_record()
+
+ def _parent_length(self, categ):
+ if not categ.parent_id:
+ return 1
+ else:
+ return 1 + self._parent_length(categ.parent_id)
+
+ def export_brand(self, brand):
+ if not brand:
+ return
+ brand_binder = self.binder_for("prestashop.product.brand")
+ ext_id = brand_binder.to_external(brand, wrap=True)
+ if ext_id:
+ return ext_id
+
+ ps_brand_obj = self.env["prestashop.product.brand"]
+ res = {
+ "backend_id": self.backend_record.id,
+ "odoo_id": brand.id,
+ "link_rewrite": get_slug(brand.name),
+ }
+ binding = ps_brand_obj.with_context(connector_no_export=True).create(res)
+ binding.export_record()
+
+ def _export_dependencies(self):
+ """Export the dependencies for the product"""
+ super()._export_dependencies()
+ attribute_binder = self.binder_for("prestashop.product.combination.option")
+ option_binder = self.binder_for("prestashop.product.combination.option.value")
+
+ for category in self.binding.categ_ids:
+ self.export_categories(category)
+
+ self.export_brand(self.binding.product_brand_id)
+
+ for line in self.binding.attribute_line_ids:
+ attribute_ext_id = attribute_binder.to_external(
+ line.attribute_id, wrap=True
+ )
+ if not attribute_ext_id:
+ self._export_dependency(
+ line.attribute_id, "prestashop.product.combination.option"
+ )
+ for value in line.value_ids:
+ value_ext_id = option_binder.to_external(value, wrap=True)
+ if not value_ext_id:
+ self._export_dependency(
+ value, "prestashop.product.combination.option.value"
+ )
+
+ def export_variants(self):
+ combination_obj = self.env["prestashop.product.combination"]
+ for product in self.binding.product_variant_ids:
+ if not product.product_template_attribute_value_ids:
+ continue
+ combination_ext = combination_obj.search(
+ [
+ ("backend_id", "=", self.backend_record.id),
+ ("odoo_id", "=", product.id),
+ ]
+ )
+ if not combination_ext:
+ combination_ext = combination_obj.with_context(
+ connector_no_export=True
+ ).create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": product.id,
+ "main_template_id": self.binding_id,
+ }
+ )
+ # If a template has been modified then always update PrestaShop
+ # combinations
+ combination_ext.with_delay(
+ priority=50, eta=timedelta(seconds=20)
+ ).export_record()
+
+ def _not_in_variant_images(self, image):
+ images = []
+ if len(self.binding.product_variant_ids) > 1:
+ for product in self.binding.product_variant_ids:
+ images.extend(product.image_ids.ids)
+ return image.id not in images
+
+ def check_images(self):
+ if self.binding.image_ids:
+ image_binder = self.binder_for("prestashop.product.image")
+ for image in self.binding.image_ids:
+ image_ext_id = image_binder.to_external(image, wrap=True)
+ # `image_ext_id` is ZERO as long as the image is not exported.
+ # Here we delay the export so,
+ # if we don't check this we create 2 records to be sync'ed
+ # and this leads to:
+ # ValueError:
+ # Expected singleton: prestashop.product.image(x, y)
+ if image_ext_id is None:
+ image_ext = (
+ self.env["prestashop.product.image"]
+ .with_context(connector_no_export=True)
+ .create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": image.id,
+ }
+ )
+ )
+ image_ext.with_delay(priority=5).export_record()
+
+ def update_quantities(self):
+ if len(self.binding.product_variant_ids) == 1:
+ product = self.binding.odoo_id.product_variant_ids[0]
+ product.update_prestashop_quantities()
+
+ def _after_export(self):
+ self.check_images()
+ self.export_variants()
+ self.update_quantities()
+ if not self.binding.date_add:
+ self.binding.with_context(
+ connector_no_export=True
+ ).date_add = fields.Datetime.now()
+
+
+class ProductTemplateExportMapper(Component):
+ _name = "prestashop.product.template.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.template"
+
+ direct = [
+ ("available_for_order", "available_for_order"),
+ ("show_price", "show_price"),
+ ("online_only", "online_only"),
+ ("weight", "weight"),
+ ("standard_price", "wholesale_price"),
+ (m2o_to_external("default_shop_id"), "id_shop_default"),
+ ("always_available", "active"),
+ ("barcode", "barcode"),
+ ("additional_shipping_cost", "additional_shipping_cost"),
+ ("minimal_quantity", "minimal_quantity"),
+ ("on_sale", "on_sale"),
+ ("date_add", "date_add"),
+ ("barcode", "ean13"),
+ (
+ m2o_to_external(
+ "prestashop_default_category_id", binding="prestashop.product.category"
+ ),
+ "id_category_default",
+ ),
+ ("state", "state"),
+ ("low_stock_threshold", "low_stock_threshold"),
+ ("default_code", "reference"),
+ (
+ m2o_to_external("product_brand_id", binding="prestashop.product.brand"),
+ "id_manufacturer",
+ ),
+ ("visibility", "visibility"),
+ ]
+ # handled by base mapping `translatable_fields`
+ _translatable_fields = [
+ ("name", "name"),
+ ("link_rewrite", "link_rewrite"),
+ ("meta_title", "meta_title"),
+ ("meta_description", "meta_description"),
+ ("meta_keywords", "meta_keywords"),
+ ("tags", "tags"),
+ ("available_now", "available_now"),
+ ("available_later", "available_later"),
+ ("description_short_html", "description_short"),
+ ("description_html", "description"),
+ ]
+
+ def _get_factor_tax(self, tax):
+ return (1 + tax.amount / 100) if tax.price_include else 1.0
+
+ @changed_by("taxes_id", "list_price")
+ @mapping
+ def list_price(self, record):
+ tax = record.taxes_id
+ pricelist = record.backend_id.pricelist_id
+ if pricelist:
+ prices = pricelist.get_products_price([record.odoo_id], [1.0], [None])
+ price_to_export = prices.get(record.odoo_id.id)
+ else:
+ price_to_export = record.list_price
+ if tax.price_include and tax.amount_type == "percent":
+ # 6 is the rounding precision used by PrestaShop for the
+ # tax excluded price. we can get back a 2 digits tax included
+ # price from the 6 digits rounded value
+ return {"price": str(round(price_to_export / self._get_factor_tax(tax), 6))}
+ else:
+ return {"price": str(price_to_export)}
+
+ def _get_product_category(self, record):
+ ext_categ_ids = []
+ binder = self.binder_for("prestashop.product.category")
+ for category in record.categ_ids:
+ ext_categ_ids.append({"id": binder.to_external(category, wrap=True)})
+ return ext_categ_ids
+
+ def _get_product_image(self, record):
+ ext_image_ids = []
+ binder = self.binder_for("prestashop.product.image")
+ for image in record.image_ids:
+ ext_image_ids.append({"id": binder.to_external(image, wrap=True)})
+ return ext_image_ids
+
+ @changed_by(
+ "attribute_line_ids",
+ "categ_ids",
+ "categ_id",
+ "image_ids",
+ )
+ @mapping
+ def associations(self, record):
+ return {
+ "associations": {
+ "categories": {"category_id": self._get_product_category(record)},
+ "images": {"image": self._get_product_image(record)},
+ }
+ }
+
+ @changed_by("taxes_id")
+ @mapping
+ def tax_ids(self, record):
+ if not record.taxes_id:
+ return
+ binder = self.binder_for("prestashop.account.tax.group")
+ ext_id = binder.to_external(record.taxes_id[:1].tax_group_id, wrap=True)
+ return {"id_tax_rules_group": ext_id}
+
+ @changed_by("available_date")
+ @mapping
+ def available_date(self, record):
+ if record.available_date:
+ return {"available_date": record.available_date.strftime("%Y-%m-%d")}
+ return {}
+
+ @mapping
+ def date_add(self, record):
+ # When export a record the date_add in PS is null.
+ return {"date_add": record.create_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}
+
+ @mapping
+ def default_image(self, record):
+ default_image = record.image_ids.filtered("front_image")[:1]
+ if default_image:
+ binder = self.binder_for("prestashop.product.image")
+ ps_image_id = binder.to_external(default_image, wrap=True)
+ if ps_image_id:
+ return {"id_default_image": ps_image_id}
+
+ @mapping
+ def low_stock_alert(self, record):
+ return {"low_stock_alert": "1" if record.low_stock_alert else "0"}
diff --git a/connector_prestashop_catalog_manager/readme/CREDITS.rst b/connector_prestashop_catalog_manager/readme/CREDITS.rst
new file mode 100644
index 000000000..f24fcbf9d
--- /dev/null
+++ b/connector_prestashop_catalog_manager/readme/CREDITS.rst
@@ -0,0 +1 @@
+The migration of this module from 10.0 to 14.0 was financially supported by Camptocamp.
diff --git a/connector_prestashop_catalog_manager/readme/USAGE.rst b/connector_prestashop_catalog_manager/readme/USAGE.rst
new file mode 100644
index 000000000..d631bf845
--- /dev/null
+++ b/connector_prestashop_catalog_manager/readme/USAGE.rst
@@ -0,0 +1,3 @@
+Inventory > Configuration
+ > Settings > Search Location > Enable
+ > Location > WH/Stock > Sync with Prestashop
diff --git a/connector_prestashop_catalog_manager/security/ir.model.access.csv b/connector_prestashop_catalog_manager/security/ir.model.access.csv
new file mode 100644
index 000000000..cb1d83273
--- /dev/null
+++ b/connector_prestashop_catalog_manager/security/ir.model.access.csv
@@ -0,0 +1,8 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_prestashop_product_category_image_user,User access on prestashop.categ.image,model_prestashop_categ_image,base.group_user,1,0,0,0
+access_prestashop_product_category_image_full,Full access on prestashop.categ.image,model_prestashop_categ_image,connector.group_connector_manager,1,1,1,1
+access_prestashop_export_multiple_products,Export access on product template,model_export_multiple_products,connector.group_connector_manager,1,1,1,1
+access_sync_products,access_sync_products,model_sync_products,connector.group_connector_manager,1,1,1,1
+access_active_deactive_products,access_active_deactive_products,model_active_deactive_products,connector.group_connector_manager,1,1,1,1
+access_wiz_prestashop_export_category,access_wiz_prestashop_export_category,model_wiz_prestashop_export_category,connector.group_connector_manager,1,1,1,1
+access_wiz_prestashop_export_product_brand,access_wiz_prestashop_export_product_brand,model_wiz_prestashop_export_product_brand,connector.group_connector_manager,1,1,1,1
diff --git a/connector_prestashop_catalog_manager/static/description/icon.png b/connector_prestashop_catalog_manager/static/description/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fc6eee5acc87931320e45247af74f5b7d4548ed
GIT binary patch
literal 8227
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
VEHKZY;1B=+002ovPDHLkV1l#7@nrx2
literal 0
HcmV?d00001
diff --git a/connector_prestashop_catalog_manager/static/description/icon.svg b/connector_prestashop_catalog_manager/static/description/icon.svg
new file mode 100644
index 000000000..29aa74d8c
--- /dev/null
+++ b/connector_prestashop_catalog_manager/static/description/icon.svg
@@ -0,0 +1,238 @@
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/tests/__init__.py b/connector_prestashop_catalog_manager/tests/__init__.py
new file mode 100644
index 000000000..bddcb98ff
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/__init__.py
@@ -0,0 +1,6 @@
+from . import test_export_product_attribute
+from . import test_export_product_category
+from . import test_export_product_image
+from . import test_export_product_product
+from . import test_export_product_template
+from . import test_export_product_brand
diff --git a/connector_prestashop_catalog_manager/tests/common.py b/connector_prestashop_catalog_manager/tests/common.py
new file mode 100644
index 000000000..46aebd6b4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/common.py
@@ -0,0 +1,30 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from os.path import dirname, join
+from unittest import mock
+
+from odoo.addons.connector_prestashop.tests.common import PrestashopTransactionCase
+
+
+class CatalogManagerTransactionCase(PrestashopTransactionCase):
+ def setUp(self):
+ super().setUp()
+ self.sync_metadata()
+ self.base_mapping()
+ self.shop_group = self.env["prestashop.shop.group"].search([])
+ self.shop = self.env["prestashop.shop"].search([])
+
+ mock_delay_record = mock.MagicMock()
+ self.instance_delay_record = mock_delay_record.return_value
+ self.patch_delay_record = mock.patch(
+ "odoo.addons.queue_job.models.base.DelayableRecordset",
+ new=mock_delay_record,
+ )
+ self.patch_delay_record.start()
+
+ self.cassette_library_dir = join(dirname(__file__), "fixtures/cassettes")
+
+ def tearDown(self):
+ super().tearDown()
+ self.patch_delay_record.stop()
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute.yaml
new file mode 100644
index 000000000..63f824e6f
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute.yaml
@@ -0,0 +1,50 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode New
+ attribute4New
+ attributeselect
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["273"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/product_options
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n",
+ }
+ headers:
+ access-time: ["1544009921"]
+ connection: [keep-alive]
+ content-length: ["652"]
+ content-sha1: [4290e789a9ce1823ce4ac91b063ca96ff36f04ea]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Wed, 05 Dec 2018 11:38:41 GMT"]
+ execution-time: ["0.02"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=785d98e9bd28108f77b731d15480adab26832755ccb4f95be30db352aeea3ac5%3Ay9dT%2FUvu1KNqFoEFFplK6NS2kY3JMur8CJSWpxVtPnDlJXtgJWHexZZTRsadIcbiwDNh4vUR3%2B5XjBpOfoNRUDLN4zMfjjUQnwJD8t04vsM%3D;
+ expires=Tue, 25-Dec-2018 11:38:41 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute_value.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute_value.yaml
new file mode 100644
index 000000000..7d91119b7
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_attribute_value.yaml
@@ -0,0 +1,46 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode 11New valueNew
+ value
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["271"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/product_option_values
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\n\n",
+ }
+ headers:
+ access-time: ["1544009922"]
+ connection: [keep-alive]
+ content-length: ["462"]
+ content-sha1: [680b0fdf6f597b19907a853063d1118004e618e6]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Wed, 05 Dec 2018 11:38:42 GMT"]
+ execution-time: ["0.015"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=a8a4d05d822236e10fc6b55aa29d11faa3716e1e99c5fb6f90388ddcc869bf44%3Ay9dT%2FUvu1KNqFoEFFplK6MdWNmB2dGM8e7QF71k0oQ%2FPRFEuTpUHkyA1D38AfRaBcdttFxJhxpUDel%2FmoANR4Bx32suWjACFgxh1quGCHB4%3D;
+ expires=Tue, 25-Dec-2018 11:38:42 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_brand.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_brand.yaml
new file mode 100644
index 000000000..5484a4b83
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_brand.yaml
@@ -0,0 +1,54 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode 1New
+ brand
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["134"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/manufacturers
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n",
+ }
+ headers:
+ access-time: ["1581009413"]
+ connection: [keep-alive]
+ content-length: ["802"]
+ content-sha1: [4c9faea22f246917d392a77fa137b7adf312b696]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Thu, 06 Feb 2020 17:16:53 GMT"]
+ execution-time: ["0.016"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-6afe1be0fe41e536d378c33ccb8576a7=70fe1813ab881a88b93cae0c0b17932ce2c31a2a23a428085e404b798b0b57f5%3AERwbwgjnbPhegaRWNyAjEBSV37qybRlOe7gRV3My9ws7tUTjvDrto87GEN6i4LKN6ERP2fCILkNsEMee7E3G5LxiZ0xlfI3ztXDxbrmmZQM%3D;
+ expires=Wed, 26-Feb-2020 17:16:53 GMT; Max-Age=1728000; path=/;
+ domain=localhost:8080; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_category.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_category.yaml
new file mode 100644
index 000000000..29183ec08
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_category.yaml
@@ -0,0 +1,69 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode New
+ category meta descriptionnew-categoryNew
+ category meta titleNew
+ category keywordsNew
+ category2111<p>New category
+ description</p>
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["654"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/categories
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\tNew
+ category
+ description
]]>\n\t\n\t\n\t\n\n\n\n\n\n\n",
+ }
+ headers:
+ access-time: ["1544010311"]
+ connection: [keep-alive]
+ content-length: ["1614"]
+ content-sha1: [73b504e1f3e7d44f8511f02cc8b1e8764b3380da]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Wed, 05 Dec 2018 11:45:11 GMT"]
+ execution-time: ["0.044"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=8a23211feb6747fd92d7117800b697fe81b83282d61b7482c14fd1156239b1ef%3Ay9dT%2FUvu1KNqFoEFFplK6GUHezpItVFXBKxry0vskUErKLdJXddpHQxmwbDxzWLGiJLOUqe2aMh%2Bsa40YPqEWMWyRAoyMLZ3CDMF1J8HvEQ%3D;
+ expires=Tue, 25-Dec-2018 11:45:11 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_image.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_image.yaml
new file mode 100644
index 000000000..8c8b5f16f
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_image.yaml
@@ -0,0 +1,225 @@
+interactions:
+ - request:
+ body: !!binary |
+ LS0tLS0tLS0tLS0tVGhJc19Jc190SGVfYm91TmRhUllfJA0KQ29udGVudC1EaXNwb3NpdGlvbjog
+ Zm9ybS1kYXRhOyAgICAgICAgICAgICAgICAgICAgIG5hbWU9ImltYWdlIjsgZmlsZW5hbWU9Imlj
+ b24uLnBuZyINCkNvbnRlbnQtVHlwZTogaW1hZ2UvcG5nDQoNColQTkcNChoKAAAADUlIRFIAAACA
+ AAAAgAgGAAAAwz5hywAAAARzQklUCAgICHwIZIgAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAZdEVY
+ dFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAdbklEQVR4nO2deZgU1dW431PV3bOzM4AI
+ QcUtJERFRRR1QBgIJMbvi5C4gLjkMyZGAdklphP2AcElMWKiIhp/CcR8iRuyyIwQ9YtKXAgoRB0W
+ GZR9hmG6p7vrnt8fszAzDNBL9Sxh3uepB7q6zrlnuk7duvfce88VWkgKT147L8sOSG/L0m+i0hu0
+ J9BJoYNAByACHAEOg+5F+FiQT1R1c6ph3cg1U4obwk5piEJOFZbkzuljYQ0VyFXoB3jjVOUovGsJ
+ qyPqPH/rqmmfuGlnTVocIEGW5s7PRswojN6GyNeTUYbAmwZ5yoTKnr+1wB90WXcL8bDk23lnWY5O
+ BUYBvoYoU6EIZK4Jlf3OLUdocYAYWTpobndEZiLcAHgaxwrdhTBp9MopzyeqqcUBomRxn8Xe9HbF
+ P1HRGUBWY9tTyRsOzo8TaSO0OEAUPDN0fm8x5nmgV2PbUg9lItw9auXkp+MRbnGAk/DskLmjVeW3
+ QHpj23JClGcR667RqyYeiUWsxQGOw7IRfl95cfqTit7c2LZEjcq7EU9k+G0rpu2NVqTFAephae78
+ DDB/BoY2ti1x8JmxZciYFZM+i+biFgeow9M5i9p4fOE1ivZpbFsS4AvbY/e/6dUJ2092odUQ1jQX
+ lvVbmGb7Qi8285sPcLoTcdY8f82sTie7sMUBKsnP8XuCmeHlwJWNbYtL9IzYnhcXf9d/wsZriwNU
+ sjMlfRbC8Ma2w2UuTStPe+REF7S0AYBnh8wdriov4f7vcUTQNxVrPZZuxlhbHStS6rMpJmR3MBad
+ FXOeqFyg0B/o7XL5AIjoLaNWTlla73fJKLA58fSwvM52RP8FtHdLpwqvC/pUwBf8650v+cuilXtm
+ 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
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["7856"]
+ Content-Type: [multipart/form-data; boundary=----------ThIs_Is_tHe_bouNdaRY_$]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/images/products/1
+ response:
+ body: {string: !!python/unicode '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '}
+ headers:
+ access-time: ["1544010333"]
+ connection: [keep-alive]
+ content-length: ["6609"]
+ content-sha1: [3a308e3ae28e34ab2eef6c8bedeacd54ba5250f3]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Wed, 05 Dec 2018 11:45:33 GMT"]
+ execution-time: ["0.104"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=955af223f6180571e5e5e318e1a81079ca7d43406aa0e6cdc920b78d05165d99%3Ay9dT%2FUvu1KNqFoEFFplK6KgmFvX9MCUWzxfVdGR2sctbyzMZQ%2FuHKRHmpCgQ6rfMv8tpY%2FD%2BCaCDSw627GwpbrPaMx1unqas4i0VMPWUblE%3D;
+ expires=Tue, 25-Dec-2018 11:45:33 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 200, message: OK}
+ - request:
+ body: null
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["0"]
+ User-Agent: [python-requests/2.11.1]
+ method: DELETE
+ uri: http://localhost:8080/api/images/products/1/68
+ response:
+ body: {string: !!python/unicode ""}
+ headers:
+ access-time: ["1544010333"]
+ connection: [keep-alive]
+ content-length: ["0"]
+ date: ["Wed, 05 Dec 2018 11:45:33 GMT"]
+ execution-time: ["0.01"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=955af223f6180571e5e5e318e1a81079ca7d43406aa0e6cdc920b78d05165d99%3Ay9dT%2FUvu1KNqFoEFFplK6KgmFvX9MCUWzxfVdGR2sctbyzMZQ%2FuHKRHmpCgQ6rfMv8tpY%2FD%2BCaCDSw627GwpbrPaMx1unqas4i0VMPWUblE%3D;
+ expires=Tue, 25-Dec-2018 11:45:33 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 200, message: OK}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_product.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_product.yaml
new file mode 100644
index 000000000..42bfa2ddc
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_product.yaml
@@ -0,0 +1,77 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode 4138411788010150demo_3_OS0.10110.020.023
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["532"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/combinations
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n",
+ }
+ headers:
+ access-time: ["1544010356"]
+ connection: [keep-alive]
+ content-length: ["1200"]
+ content-sha1: [7a46cf797b3653f5c063bf16e0a898a08fc411cb]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Wed, 05 Dec 2018 11:45:56 GMT"]
+ execution-time: ["0.026"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=a6ba7c94454759752a139fc6fdae9b327c99c9b1c42f846b56f69caf29139e71%3Ay9dT%2FUvu1KNqFoEFFplK6Otra9jGVczMTxQ0zVLeniD4jwMTT5sp%2F2lnJQCK1O3fW7QmPITjvETXHqyribPUUOy6ImdWABG%2F43xhrzi%2BccQ%3D;
+ expires=Tue, 25-Dec-2018 11:45:56 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+ - request:
+ body: null
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["0"]
+ User-Agent: [python-requests/2.11.1]
+ method: DELETE
+ uri: http://localhost:8080/api/combinations/60
+ response:
+ body: {string: !!python/unicode ""}
+ headers:
+ access-time: ["1544010356"]
+ connection: [keep-alive]
+ content-length: ["0"]
+ date: ["Wed, 05 Dec 2018 11:45:56 GMT"]
+ execution-time: ["0.018"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-ac85e42aef73418ac8f31f6687398719=a6ba7c94454759752a139fc6fdae9b327c99c9b1c42f846b56f69caf29139e71%3Ay9dT%2FUvu1KNqFoEFFplK6Otra9jGVczMTxQ0zVLeniD4jwMTT5sp%2F2lnJQCK1O3fW7QmPITjvETXHqyribPUUOy6ImdWABG%2F43xhrzi%2BccQ%3D;
+ expires=Tue, 25-Dec-2018 11:45:56 GMT; Max-Age=1728000; path=/; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 200, message: OK}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_template.yaml b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_template.yaml
new file mode 100644
index 000000000..eb9b33fcb
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/cassettes/test_export_product_template.yaml
@@ -0,0 +1,110 @@
+interactions:
+ - request:
+ body:
+ !!python/unicode 2020-02-06
+ 16:57:248411788010150New
+ product meta
+ title2new-product<p>New product
+ description</p>1110.020.084117880101505New product meta
+ keywords2016-08-2911NEW_PRODUCTNew productNew
+ product meta
+ description01.00.11111New product
+ tags2345New product available
+ now<p>New
+ product description
+ short</p>New product available
+ later
+ headers:
+ Accept: ["*/*"]
+ Accept-Encoding: ["gzip, deflate"]
+ Connection: [keep-alive]
+ Content-Length: ["1845"]
+ Content-Type: [text/xml]
+ User-Agent: [python-requests/2.11.1]
+ method: POST
+ uri: http://localhost:8080/api/products
+ response:
+ body:
+ {
+ string:
+ !!python/unicode "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\tNew product
+ description
]]>\n\tNew product
+ description
+ short]]>\n\t\n\t\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n\n\n\t\n\t\n\t\n\t\n\n\n\n\n\n\n",
+ }
+ headers:
+ access-time: ["1581008244"]
+ connection: [keep-alive]
+ content-length: ["5218"]
+ content-sha1: [c70fd4a619a4a4bf71229b57e465b3aace21f256]
+ content-type: [text/xml;charset=utf-8]
+ date: ["Thu, 06 Feb 2020 16:57:24 GMT"]
+ execution-time: ["0.051"]
+ psws-version: [1.6.1.22]
+ server: [nginx/1.10.3 (Ubuntu)]
+ set-cookie:
+ [
+ "PrestaShop-6afe1be0fe41e536d378c33ccb8576a7=99ef58a58fe325ebc994812ea04fec000e7d9e4532b86e166abaa438d78e6711%3AERwbwgjnbPhegaRWNyAjEDGs9Ou1AWI7j6aOPwsre8LNHD2QLBTUTdzjB3g7IWQeicISDrhEhOu4WTKS9MpUFEgN1N3HM7BH145oU1%2FGeuo%3D;
+ expires=Wed, 26-Feb-2020 16:57:24 GMT; Max-Age=1728000; path=/;
+ domain=localhost:8080; HttpOnly",
+ ]
+ transfer-encoding: [chunked]
+ vary: [Accept-Encoding]
+ x-powered-by: [PrestaShop Webservice]
+ status: {code: 201, message: Created}
+version: 1
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_attribute.py b/connector_prestashop_catalog_manager/tests/test_export_product_attribute.py
new file mode 100644
index 000000000..5578f05a0
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_attribute.py
@@ -0,0 +1,190 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProductAttribute(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind attribute
+ attribute_size = self.env["product.attribute"].create(
+ {
+ "name": "Size",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.combination.option", attribute_size.id, 1
+ )
+
+ # create attribute and value
+ self.attribute = self.env["product.attribute"].create(
+ {
+ "name": "New attribute",
+ }
+ )
+ self.value = self.env["product.attribute.value"].create(
+ {
+ "attribute_id": attribute_size.id,
+ "name": "New value",
+ }
+ )
+
+ def _bind_attribute(self):
+ return self.create_binding_no_export(
+ "prestashop.product.combination.option", self.attribute.id, 4
+ ).with_context(connector_no_export=False)
+
+ def _bind_value(self):
+ return self.create_binding_no_export(
+ "prestashop.product.combination.option.value", self.value.id, 25
+ ).with_context(connector_no_export=False)
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_onbind(self):
+ # create attribute binding
+ self.env["prestashop.product.combination.option"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.attribute.id,
+ }
+ )
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_value_onbind(self):
+ # bind attribute
+ self._bind_attribute()
+ # create value binding
+ self.env["prestashop.product.combination.option.value"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.value.id,
+ }
+ )
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_onwrite(self):
+ # bind attribute
+ self._bind_attribute()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+ # write in value
+ self.attribute.name = "New attribute updated"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # write in binding
+ # binding.display_type = "radio" --> This triggered below 2 events
+ # attribute.event.listener.on_record_write calling export_record
+ # prestashop.attribute.event.listener.on_record_write calling export_record
+ self.attribute.display_type = "radio"
+ # check export delayed again
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_value_onwrite(self):
+ # bind attribute and value
+ self._bind_attribute()
+ binding = self._bind_value()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+ # write in value
+ self.value.name = "New value updated"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # write in binding
+ binding.prestashop_position = 2
+ # check export delayed again
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_job(self):
+ # create attribute binding
+ binding = self.env["prestashop.product.combination.option"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.attribute.id,
+ "group_type": "select",
+ "prestashop_position": 4,
+ }
+ )
+ # export attribute
+ with recorder.use_cassette(
+ "test_export_product_attribute",
+ cassette_library_dir=self.cassette_library_dir,
+ ) as cassette:
+ binding.export_record()
+
+ # check request
+ self.assertEqual(1, len(cassette.requests))
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/product_options", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_option = body["prestashop"]["product_options"]
+ # check basic fields
+ for field, value in list(
+ {
+ "group_type": "select",
+ "position": "4",
+ }.items()
+ ):
+ self.assertEqual(value, ps_option[field])
+ # check translatable fields
+ for field, value in list(
+ {
+ "name": "New attribute",
+ "public_name": "New attribute",
+ }.items()
+ ):
+ self.assertEqual(value, ps_option[field]["language"]["value"])
+
+ @assert_no_job_delayed
+ def test_export_product_attribute_value_job(self):
+ # create value binding
+ binding = self.env["prestashop.product.combination.option.value"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.value.id,
+ }
+ )
+ # export value
+ with recorder.use_cassette(
+ "test_export_product_attribute_value",
+ cassette_library_dir=self.cassette_library_dir,
+ ) as cassette:
+ binding.export_record()
+
+ # check request
+ self.assertEqual(1, len(cassette.requests))
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/product_option_values", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_option = body["prestashop"]["product_option_value"]
+ # check basic fields
+ for field, value in list(
+ {
+ "id_attribute_group": "1",
+ "value": "New value",
+ }.items()
+ ):
+ self.assertEqual(value, ps_option[field])
+ # check translatable fields
+ for field, value in list(
+ {
+ "name": "New value",
+ }.items()
+ ):
+ self.assertEqual(value, ps_option[field]["language"]["value"])
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_brand.py b/connector_prestashop_catalog_manager/tests/test_export_product_brand.py
new file mode 100644
index 000000000..973b36023
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_brand.py
@@ -0,0 +1,93 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProductBrand(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind parent
+ parent = self.env["product.brand"].create({"name": "Home"})
+ self.create_binding_no_export("prestashop.product.brand", parent.id, 2)
+
+ # Create a product brand to export:
+ self.brand = self.env["product.brand"].create(
+ {
+ "name": "New brand",
+ }
+ )
+
+ def _bind_brand(self):
+ return self.create_binding_no_export(
+ "prestashop.product.brand", self.brand.id, 12
+ ).with_context(connector_no_export=False)
+
+ @assert_no_job_delayed
+ def test_export_product_brand_wizard(self):
+ # export from wizard
+ wizard = (
+ self.env["wiz.prestashop.export.product.brand"]
+ .with_context(active_ids=[self.brand.id])
+ .create({})
+ )
+ wizard.export_product_brands()
+
+ # check binding created
+ bindings = self.env["prestashop.product.brand"].search(
+ [("odoo_id", "=", self.brand.id)]
+ )
+ self.assertEqual(1, len(bindings))
+ # check export delayed
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=["backend_id", "odoo_id"]
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_brand_onwrite(self):
+ self._bind_brand()
+
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+
+ # write in brand
+ self.brand.name = "New brand updated"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_brand_job(self):
+ # create binding
+ binding = self.env["prestashop.product.brand"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.brand.id,
+ }
+ )
+ # export brand
+ with recorder.use_cassette(
+ "test_export_product_brand", cassette_library_dir=self.cassette_library_dir
+ ) as cassette:
+ binding.export_record()
+
+ # check request
+ self.assertEqual(1, len(cassette.requests))
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/manufacturers", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_brand = body["prestashop"]["manufacturers"]
+ # check name
+ for field, value in list(
+ {
+ "name": "New brand",
+ }.items()
+ ):
+ self.assertEqual(value, ps_brand[field])
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_category.py b/connector_prestashop_catalog_manager/tests/test_export_product_category.py
new file mode 100644
index 000000000..1509f5285
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_category.py
@@ -0,0 +1,124 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from ..models.product_template.exporter import get_slug
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProductCategory(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind parent
+ parent = self.env["product.category"].create({"name": "Home"})
+ self.create_binding_no_export("prestashop.product.category", parent.id, 2)
+
+ # Create a product category to export:
+ self.category = self.env["product.category"].create(
+ {
+ "name": "New category",
+ "parent_id": parent.id,
+ }
+ )
+
+ def _bind_category(self):
+ return self.create_binding_no_export(
+ "prestashop.product.category", self.category.id, 12
+ ).with_context(connector_no_export=False)
+
+ @assert_no_job_delayed
+ def test_export_product_category_wizard(self):
+ # export from wizard
+ wizard = (
+ self.env["wiz.prestashop.export.category"]
+ .with_context(active_ids=[self.category.id])
+ .create({})
+ )
+ wizard.export_categories()
+
+ # check binding created
+ bindings = self.env["prestashop.product.category"].search(
+ [("odoo_id", "=", self.category.id)]
+ )
+ self.assertEqual(1, len(bindings))
+ # check export delayed
+ # sequence of fields is from ./wizards/export_category.py
+ # > def export_categories
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=["backend_id", "default_shop_id", "link_rewrite", "odoo_id"]
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_category_onwrite(self):
+ # bind category
+ binding = self._bind_category()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+
+ # write in category
+ self.category.name = "New category updated"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # write in binding
+ binding.description = "New category description updated"
+ # check export delayed again
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_category_job(self):
+ # create binding
+ binding = self.env["prestashop.product.category"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.category.id,
+ "default_shop_id": self.shop.id,
+ "description": "New category description",
+ "link_rewrite": get_slug(self.category.name),
+ "meta_description": "New category meta description",
+ "meta_keywords": "New category keywords",
+ "meta_title": "New category meta title",
+ "position": 1,
+ }
+ )
+ # export category
+ with recorder.use_cassette(
+ "test_export_product_category",
+ cassette_library_dir=self.cassette_library_dir,
+ ) as cassette:
+ binding.export_record()
+
+ # check request
+ self.assertEqual(1, len(cassette.requests))
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/categories", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_category = body["prestashop"]["category"]
+ # check basic fields
+ for field, value in list(
+ {
+ "active": "1",
+ "id_parent": "2",
+ "id_shop_default": "1",
+ "position": "1",
+ }.items()
+ ):
+ self.assertEqual(value, ps_category[field])
+ # check translatable fields
+ for field, value in list(
+ {
+ "description": "New category description
",
+ "link_rewrite": "new-category",
+ "meta_description": "New category meta description",
+ "meta_keywords": "New category keywords",
+ "meta_title": "New category meta title",
+ "name": "New category",
+ }.items()
+ ):
+ self.assertEqual(value, ps_category[field]["language"]["value"])
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_image.py b/connector_prestashop_catalog_manager/tests/test_export_product_image.py
new file mode 100644
index 000000000..54a6249a4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_image.py
@@ -0,0 +1,147 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.modules.module import get_resource_path
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProductImage(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind template
+ template = self.env["product.template"].create(
+ {
+ "name": "Faded Short Sleeves T-shirt",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.template",
+ template.id,
+ 1,
+ **{
+ "default_shop_id": self.shop.id,
+ "link_rewrite": "faded-short-sleaves-t-shirt",
+ }
+ )
+
+ # create image and binding
+ self.image = self.env["base_multi_image.image"].create(
+ {
+ "owner_id": template.id,
+ "owner_model": "product.template",
+ "storage": "file",
+ "path": get_resource_path(
+ "connector_prestashop", "static", "description", "icon.png"
+ ),
+ }
+ )
+ self.binding = self.create_binding_no_export(
+ "prestashop.product.image", self.image.id, None
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_image_onwrite(self):
+ # write in image
+ self.image.write(
+ {
+ "path": get_resource_path(
+ "connector_prestashop_catalog_manager",
+ "static",
+ "description",
+ "icon.png",
+ ),
+ }
+ )
+ # check export delayed
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=[
+ "path",
+ ]
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_image_ondelete(self):
+ # bind image
+ self.binding.prestashop_id = 24
+
+ # delete image
+ self.image.unlink()
+ # check export delete delayed
+ self.instance_delay_record.export_delete_record.assert_called_once_with(
+ self.backend_record, 24, {"id_product": 1}
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_image_jobs(self):
+ with recorder.use_cassette(
+ "test_export_product_image", cassette_library_dir=self.cassette_library_dir
+ ) as cassette:
+
+ # create image in PS
+ self.binding.export_record()
+
+ # check POST request
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/images/products/1", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+
+ # VCR.py does not support urllib v1 request in
+ # OCA/server-tools/base_multi_image/models/image.py:
+ # to get image from URL so...
+
+ # ...update test is avoided
+ # update image in PS
+ # prestashop_id = self.binding.prestashop_id
+ # self.binding.export_record()
+ #
+ # # check DELETE requests
+ # request = cassette.requests[1]
+ # self.assertEqual('DELETE', request.method)
+ # self.assertEqual(
+ # '/api/images/products/1/%s' % prestashop_id,
+ # self.parse_path(request.uri))
+ # self.assertDictEqual({}, self.parse_qs(request.uri))
+ #
+ # # check POST request
+ # request = cassette.requests[2]
+ # self.assertEqual('POST', request.method)
+ # self.assertEqual('/api/images/products/1',
+ # self.parse_path(request.uri))
+ # self.assertDictEqual({}, self.parse_qs(request.uri))
+
+ # ...and delete test is hacked
+ self.image.write(
+ {
+ "storage": "file",
+ "path": get_resource_path(
+ "connector_prestashop", "static", "description", "icon.png"
+ ),
+ }
+ )
+
+ # delete image in PS
+ attributes = {
+ "id_product": 1,
+ }
+ self.env["prestashop.product.image"].export_delete_record(
+ self.backend_record,
+ self.binding.prestashop_id,
+ attributes,
+ )
+
+ # check DELETE requests
+ request = cassette.requests[1]
+ self.assertEqual("DELETE", request.method)
+ self.assertEqual(
+ "/api/images/products/1/%s" % self.binding.prestashop_id,
+ self.parse_path(request.uri),
+ )
+ self.assertDictEqual({}, self.parse_qs(request.uri))
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_product.py b/connector_prestashop_catalog_manager/tests/test_export_product_product.py
new file mode 100644
index 000000000..87a58ee6a
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_product.py
@@ -0,0 +1,228 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from unittest import mock
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProductProduct(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind color attribute
+ color_attribute = self.env["product.attribute"].create(
+ {
+ "name": "Color",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.combination.option", color_attribute.id, 3
+ )
+
+ # create and bind color value
+ color_value = self.env["product.attribute.value"].create(
+ {
+ "attribute_id": color_attribute.id,
+ "name": "Orange",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.combination.option.value", color_value.id, 13
+ )
+
+ # create and bind size attribute
+ size_attribute = self.env["product.attribute"].create(
+ {
+ "name": "Size",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.combination.option", size_attribute.id, 1
+ )
+
+ # create and bind size value
+ size_value = self.env["product.attribute.value"].create(
+ {
+ "attribute_id": size_attribute.id,
+ "name": "One size",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.combination.option.value", size_value.id, 4
+ )
+
+ # create and bind template
+ template = self.env["product.template"].create(
+ {
+ "name": "Printed Dress",
+ }
+ )
+ self.main_template_id = self.create_binding_no_export(
+ "prestashop.product.template",
+ template.id,
+ 3,
+ **{
+ "default_shop_id": self.shop.id,
+ "link_rewrite": "printed-dress",
+ "attribute_line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "attribute_id": color_attribute.id,
+ "value_ids": [(6, 0, [color_value.id])],
+ },
+ ),
+ (
+ 0,
+ 0,
+ {
+ "attribute_id": size_attribute.id,
+ "value_ids": [(6, 0, [size_value.id])],
+ },
+ ),
+ ],
+ }
+ )
+
+ # update product
+ self.product = template.product_variant_ids[0]
+ self.product.write(
+ {
+ "barcode": "8411788010150",
+ "default_code": "demo_3_OS",
+ "default_on": False,
+ "impact_price": 20.0,
+ "product_tmpl_id": template.id,
+ "standard_price": 10.0,
+ "weight": 0.1,
+ }
+ )
+
+ def _bind_product(self):
+ return self.create_binding_no_export(
+ "prestashop.product.combination",
+ self.product.id,
+ None,
+ **{
+ "main_template_id": self.main_template_id.id,
+ "minimal_quantity": 2,
+ }
+ ).with_context(connector_no_export=False)
+
+ def test_export_product_product_oncreate(self):
+ # create binding
+ self.env["prestashop.product.combination"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.product.id,
+ "main_template_id": self.main_template_id.id,
+ }
+ )
+ # check export delayed
+ # The sequence of fields should follow above create
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=["backend_id", "odoo_id", "main_template_id"]
+ )
+
+ def test_export_product_product_onwrite(self):
+ # reset mock:
+ self.patch_delay_record.stop()
+ mock_delay_record = mock.MagicMock()
+ self.instance_delay_record = mock_delay_record.return_value
+ self.patch_delay_record = mock.patch(
+ "odoo.addons.queue_job.models.base.DelayableRecordset",
+ new=mock_delay_record,
+ )
+ self.patch_delay_record.start()
+
+ # bind product
+ binding = self._bind_product()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+ # write in product
+ self.product.default_code = "demo_3_OS"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # write in binding
+ binding.minimal_quantity = 2
+ # check export delayed
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_product_ondelete(self):
+ # bind product
+ binding = self._bind_product()
+ binding.prestashop_id = 46
+ backend_id = binding.backend_id
+ # delete product
+ self.product.unlink()
+ # check export delete delayed
+ self.instance_delay_record.export_delete_record.assert_any_call(backend_id, 46)
+ self.instance_delay_record.export_delete_record.assert_called_with(
+ backend_id, 3
+ )
+ assert self.instance_delay_record.export_delete_record.call_count == 2
+
+ @assert_no_job_delayed
+ def test_export_product_product_jobs(self):
+ # bind product
+ binding = self._bind_product()
+
+ with recorder.use_cassette(
+ "test_export_product_product",
+ cassette_library_dir=self.cassette_library_dir,
+ ) as cassette:
+
+ # create combination in PS
+ binding.export_record()
+
+ # check POST request
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/combinations", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_product = body["prestashop"]["combination"]
+ # check basic fields
+ for field, value in list(
+ {
+ "active": "1",
+ "default_on": "0",
+ "ean13": "8411788010150",
+ "id_product": "3",
+ "minimal_quantity": "2",
+ "price": "20.0",
+ "reference": "demo_3_OS",
+ "weight": "0.1",
+ "wholesale_price": "10.0",
+ }.items()
+ ):
+ self.assertEqual(value, ps_product[field])
+ # check option values
+ ps_product_option_values = ps_product["associations"][
+ "product_option_values"
+ ]["product_option_value"]
+ self.assertIn({"id": "4"}, ps_product_option_values)
+ self.assertIn({"id": "13"}, ps_product_option_values)
+
+ # delete combination in PS
+ self.env["prestashop.product.combination"].export_delete_record(
+ self.backend_record,
+ binding.prestashop_id,
+ )
+
+ # check DELETE requests
+ request = cassette.requests[1]
+ self.assertEqual("DELETE", request.method)
+ self.assertEqual(
+ "/api/combinations/{}".format(binding.prestashop_id),
+ self.parse_path(request.uri),
+ )
+ self.assertDictEqual({}, self.parse_qs(request.uri))
diff --git a/connector_prestashop_catalog_manager/tests/test_export_product_template.py b/connector_prestashop_catalog_manager/tests/test_export_product_template.py
new file mode 100644
index 000000000..e638318db
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_template.py
@@ -0,0 +1,266 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from ..models.product_template.exporter import get_slug
+from .common import CatalogManagerTransactionCase
+
+
+class TestExportProduct(CatalogManagerTransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ # create and bind category
+ category_home = self.env["product.category"].create(
+ {
+ "name": "Home",
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.category", category_home.id, 2
+ )
+ category_women = self.env["product.category"].create(
+ {
+ "name": "Women",
+ "parent_id": category_home.id,
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.category", category_women.id, 3
+ )
+ category_tops = self.env["product.category"].create(
+ {
+ "name": "Tops",
+ "parent_id": category_women.id,
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.category", category_tops.id, 4
+ )
+ category_tshirts = self.env["product.category"].create(
+ {
+ "name": "T-shirts",
+ "parent_id": category_tops.id,
+ }
+ )
+ self.create_binding_no_export(
+ "prestashop.product.category", category_tshirts.id, 5
+ )
+
+ acme_brand = self.env["product.brand"].create(
+ {
+ "name": "ACME",
+ }
+ )
+ self.create_binding_no_export("prestashop.product.brand", acme_brand.id, 1)
+
+ # create template
+ self.template = self.env["product.template"].create(
+ {
+ "barcode": "8411788010150",
+ "categ_ids": [
+ (
+ 6,
+ False,
+ [
+ category_home.id,
+ category_women.id,
+ category_tops.id,
+ category_tshirts.id,
+ ],
+ )
+ ],
+ "default_code": "NEW_PRODUCT",
+ "list_price": 20.0,
+ "name": "New product",
+ "prestashop_default_category_id": category_tshirts.id,
+ "standard_price": 10.0,
+ "weight": 0.1,
+ "product_brand_id": acme_brand.id,
+ }
+ )
+
+ def _bind_template(self):
+ return self.create_binding_no_export(
+ "prestashop.product.template",
+ self.template.id,
+ 8,
+ **{
+ "default_shop_id": self.shop.id,
+ "link_rewrite": "new-product",
+ }
+ ).with_context(connector_no_export=False)
+
+ @assert_no_job_delayed
+ def test_export_product_template_wizard_export(self):
+ # export from wizard
+ wizard = (
+ self.env["export.multiple.products"]
+ .with_context(active_ids=[self.template.id])
+ .create({})
+ )
+ wizard.export_products()
+
+ # check binding created
+ binding_model = "prestashop.product.template"
+ bindings = self.env[binding_model].search([("odoo_id", "=", self.template.id)])
+ self.assertEqual(1, len(bindings))
+ # check export delayed
+ # sequence of fields is from ./wizards/export_multiple_products.py
+ # > def create_prestashop_template
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=["backend_id", "default_shop_id", "link_rewrite", "odoo_id"]
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_template_wizard_active(self):
+ # bind template
+ self._bind_template()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+ # deactivate from wizard
+ wizard = (
+ self.env["active.deactive.products"]
+ .with_context(active_ids=[self.template.id])
+ .create({})
+ )
+ wizard.deactive_products()
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # deactivate again
+ wizard.deactive_products()
+ # check no export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # force deactivate
+ wizard.force_status = True
+ wizard.deactive_products()
+ # check export delayed
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+ # activate from wizard
+ wizard.force_status = False
+ wizard.active_products()
+ # check export delayed
+ self.assertEqual(3, self.instance_delay_record.export_record.call_count)
+ # activate again
+ wizard.active_products()
+ # check no export delayed
+ self.assertEqual(3, self.instance_delay_record.export_record.call_count)
+ # force activate
+ wizard.force_status = True
+ wizard.active_products()
+ # check export delayed
+ self.assertEqual(4, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_template_wizard_resync(self):
+ # bind template
+ self._bind_template()
+ # resync from wizard
+ wizard = (
+ self.env["sync.products"]
+ .with_context(active_ids=[self.template.id], connector_delay=True)
+ .create({})
+ )
+ wizard.sync_products()
+ # check import done
+ self.instance_delay_record.import_record.assert_called_once_with(
+ self.backend_record, 8
+ )
+
+ @assert_no_job_delayed
+ def test_export_product_template_onwrite(self):
+ # bind template
+ binding = self._bind_template()
+ # check no export delayed
+ self.assertEqual(0, self.instance_delay_record.export_record.call_count)
+ # write in template
+ self.template.name = "New product updated"
+ # check export delayed
+ self.assertEqual(1, self.instance_delay_record.export_record.call_count)
+ # write in binding
+ binding.meta_title = "New product meta title updated"
+ # check export delayed
+ self.assertEqual(2, self.instance_delay_record.export_record.call_count)
+
+ @assert_no_job_delayed
+ def test_export_product_template_job(self):
+ # create binding
+ binding = self.env["prestashop.product.template"].create(
+ {
+ "backend_id": self.backend_record.id,
+ "odoo_id": self.template.id,
+ "additional_shipping_cost": 1.0,
+ "always_available": True,
+ "available_date": "2016-08-29",
+ "available_later": "New product available later",
+ "available_now": "New product available now",
+ "default_shop_id": self.shop.id,
+ "description_html": "New product description",
+ "description_short_html": "New product description short",
+ "link_rewrite": get_slug(self.template.name),
+ "meta_title": "New product meta title",
+ "meta_description": "New product meta description",
+ "meta_keywords": "New product meta keywords",
+ "minimal_quantity": 2,
+ "on_sale": True,
+ "online_only": True,
+ "tags": "New product tags",
+ }
+ )
+ # export template
+ with recorder.use_cassette(
+ "test_export_product_template",
+ cassette_library_dir=self.cassette_library_dir,
+ ) as cassette:
+ binding.export_record()
+
+ # check request
+ self.assertEqual(1, len(cassette.requests))
+ request = cassette.requests[0]
+ self.assertEqual("POST", request.method)
+ self.assertEqual("/api/products", self.parse_path(request.uri))
+ self.assertDictEqual({}, self.parse_qs(request.uri))
+ body = self.xmltodict(request.body)
+ ps_product = body["prestashop"]["product"]
+ # check basic fields
+ for field, value in list(
+ {
+ "active": "1",
+ "additional_shipping_cost": "1.0",
+ "available_date": "2016-08-29",
+ "available_for_order": "1",
+ "barcode": "8411788010150",
+ "id_category_default": "5",
+ "id_shop_default": "1",
+ "minimal_quantity": "2",
+ "on_sale": "1",
+ "online_only": "1",
+ "price": "20.0",
+ "reference": "NEW_PRODUCT",
+ "show_price": "1",
+ "weight": "0.1",
+ "wholesale_price": "10.0",
+ "id_manufacturer": "1",
+ }.items()
+ ):
+ self.assertEqual(value, ps_product[field])
+ # check translatable fields
+ for field, value in list(
+ {
+ "available_later": "New product available later",
+ "available_now": "New product available now",
+ "description": "New product description
",
+ "description_short": "New product description short" "
",
+ "link_rewrite": "new-product",
+ "meta_description": "New product meta description",
+ "meta_keywords": "New product meta keywords",
+ "meta_title": "New product meta title",
+ "name": "New product",
+ "tags": "New product tags",
+ }.items()
+ ):
+ self.assertEqual(value, ps_product[field]["language"]["value"])
diff --git a/connector_prestashop_catalog_manager/views/product_attribute_view.xml b/connector_prestashop_catalog_manager/views/product_attribute_view.xml
new file mode 100644
index 000000000..bde69d5c4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/views/product_attribute_view.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ product.attribute.form
+ product.attribute
+
+
+
+
+
+
+ prestashop.product.combination.option
+
+
+
+
+
+
+ prestashop.product.combination.option
+
+
+
+
+
+
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/views/product_category_view.xml b/connector_prestashop_catalog_manager/views/product_category_view.xml
new file mode 100644
index 000000000..0468370ac
--- /dev/null
+++ b/connector_prestashop_catalog_manager/views/product_category_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ connector_prestashop.product.category.tree
+ prestashop.product.category
+
+
+
+ 1
+
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/views/product_image_view.xml b/connector_prestashop_catalog_manager/views/product_image_view.xml
new file mode 100644
index 000000000..61624ece4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/views/product_image_view.xml
@@ -0,0 +1,13 @@
+
+
+
+ connector_prestashop.product.image.form
+ base_multi_image.image
+
+
+
+
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/views/product_view.xml b/connector_prestashop_catalog_manager/views/product_view.xml
new file mode 100644
index 000000000..ca8d5d131
--- /dev/null
+++ b/connector_prestashop_catalog_manager/views/product_view.xml
@@ -0,0 +1,46 @@
+
+
+
+ connector_prestashop.product.template.form
+
+ prestashop.product.template
+
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ connector_prestashop.product.template.tree
+
+ prestashop.product.template
+
+
+
+ 1
+
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/__init__.py b/connector_prestashop_catalog_manager/wizards/__init__.py
new file mode 100644
index 000000000..720ac0d4e
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/__init__.py
@@ -0,0 +1,7 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import export_multiple_products
+from . import sync_products
+from . import active_deactive_products
+from . import export_category
+from . import export_brand
diff --git a/connector_prestashop_catalog_manager/wizards/active_deactive_products.py b/connector_prestashop_catalog_manager/wizards/active_deactive_products.py
new file mode 100644
index 000000000..e97898688
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/active_deactive_products.py
@@ -0,0 +1,29 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class SyncProducts(models.TransientModel):
+ _name = "active.deactive.products"
+ _description = "Activate/Deactivate Products"
+
+ force_status = fields.Boolean(
+ string="Force Status",
+ help="Check this option to force active product in prestashop",
+ )
+
+ def _change_status(self, status):
+ self.ensure_one()
+ product_obj = self.env["product.template"]
+ for product in product_obj.browse(self.env.context["active_ids"]):
+ for bind in product.prestashop_bind_ids:
+ if bind.always_available != status or self.force_status:
+ bind.always_available = status
+
+ def active_products(self):
+ for product in self:
+ product._change_status(True)
+
+ def deactive_products(self):
+ for product in self:
+ product._change_status(False)
diff --git a/connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml b/connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml
new file mode 100644
index 000000000..4da7b2838
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml
@@ -0,0 +1,72 @@
+
+
+
+ active.product.form
+ active.deactive.products
+
+
+
+
+
+ Active Products
+ active.deactive.products
+
+ form
+ form
+ new
+
+
+
+
+ deactive.product.form
+ active.deactive.products
+
+
+
+
+
+ Deactive Products
+ active.deactive.products
+
+ form
+ form
+ new
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/export_brand.py b/connector_prestashop_catalog_manager/wizards/export_brand.py
new file mode 100644
index 000000000..4557672ee
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_brand.py
@@ -0,0 +1,45 @@
+# Copyright 2020 PlanetaTIC - Marc Poch
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class PrestashopExportProductBrand(models.TransientModel):
+ _name = "wiz.prestashop.export.product.brand"
+ _description = "Prestashop Export Product Brand"
+
+ def _default_backend(self):
+ return self.env["prestashop.backend"].search([], limit=1).id
+
+ def _default_shop(self):
+ return self.env["prestashop.shop"].search([], limit=1).id
+
+ backend_id = fields.Many2one(
+ comodel_name="prestashop.backend",
+ default=_default_backend,
+ string="Backend",
+ )
+ shop_id = fields.Many2one(
+ comodel_name="prestashop.shop",
+ default=_default_shop,
+ string="Shop",
+ )
+
+ def export_product_brands(self):
+ self.ensure_one()
+ brand_obj = self.env["product.brand"]
+ ps_product_brand_obj = self.env["prestashop.product.brand"]
+ for prod_brand in brand_obj.browse(self.env.context["active_ids"]):
+ ps_product_brand = ps_product_brand_obj.search(
+ [
+ ("odoo_id", "=", prod_brand.id),
+ ("backend_id", "=", self.backend_id.id),
+ ]
+ )
+ if not ps_product_brand:
+ ps_product_brand_obj.create(
+ {
+ "backend_id": self.backend_id.id,
+ "odoo_id": prod_brand.id,
+ }
+ )
diff --git a/connector_prestashop_catalog_manager/wizards/export_brand_view.xml b/connector_prestashop_catalog_manager/wizards/export_brand_view.xml
new file mode 100644
index 000000000..94cf2aca3
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_brand_view.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ wiz.prestashop.export.product.brand.form
+ wiz.prestashop.export.product.brand
+
+
+
+
+
+ Export To PrestaShop
+ wiz.prestashop.export.product.brand
+
+ form
+ form
+ new
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/export_category.py b/connector_prestashop_catalog_manager/wizards/export_category.py
new file mode 100644
index 000000000..bde658155
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_category.py
@@ -0,0 +1,49 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+from ..models.product_template.exporter import get_slug
+
+
+class PrestashopExportCategory(models.TransientModel):
+ _name = "wiz.prestashop.export.category"
+ _description = "Prestashop Export Category"
+
+ def _default_backend(self):
+ return self.env["prestashop.backend"].search([], limit=1).id
+
+ def _default_shop(self):
+ return self.env["prestashop.shop"].search([], limit=1).id
+
+ backend_id = fields.Many2one(
+ comodel_name="prestashop.backend",
+ default=_default_backend,
+ string="Backend",
+ )
+ shop_id = fields.Many2one(
+ comodel_name="prestashop.shop",
+ default=_default_shop,
+ string="Shop",
+ )
+
+ def export_categories(self):
+ self.ensure_one()
+ category_obj = self.env["product.category"]
+ ps_category_obj = self.env["prestashop.product.category"]
+ for category in category_obj.browse(self.env.context["active_ids"]):
+ ps_category = ps_category_obj.search(
+ [
+ ("odoo_id", "=", category.id),
+ ("backend_id", "=", self.backend_id.id),
+ ("default_shop_id", "=", self.shop_id.id),
+ ]
+ )
+ if not ps_category:
+ ps_category_obj.create(
+ {
+ "backend_id": self.backend_id.id,
+ "default_shop_id": self.shop_id.id,
+ "link_rewrite": get_slug(category.name),
+ "odoo_id": category.id,
+ }
+ )
diff --git a/connector_prestashop_catalog_manager/wizards/export_category_view.xml b/connector_prestashop_catalog_manager/wizards/export_category_view.xml
new file mode 100644
index 000000000..d45d45375
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_category_view.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ wiz.prestashop.export.category.form
+ wiz.prestashop.export.category
+
+
+
+
+
+
+ Export To PrestaShop
+ wiz.prestashop.export.category
+ form
+ new
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/export_multiple_products.py b/connector_prestashop_catalog_manager/wizards/export_multiple_products.py
new file mode 100644
index 000000000..8416d02ca
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_multiple_products.py
@@ -0,0 +1,157 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import re
+import unicodedata
+from functools import reduce
+
+from odoo import _, fields, models
+from odoo.exceptions import ValidationError
+
+try:
+ import slugify as slugify_lib
+except ImportError:
+ slugify_lib = None
+
+
+def get_slug(name):
+ if slugify_lib:
+ try:
+ return slugify_lib.slugify(name)
+ except TypeError:
+ pass
+ uni = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
+ slug = re.sub(r"[\W_]", " ", uni).strip().lower()
+ slug = re.sub(r"[-\s]+", "-", slug)
+ return slug
+
+
+class ExportMultipleProducts(models.TransientModel):
+ _name = "export.multiple.products"
+ _description = "Export Multiple Products"
+
+ def _default_backend(self):
+ return self.env["prestashop.backend"].search([], limit=1).id
+
+ def _default_shop(self):
+ return self.env["prestashop.shop"].search([], limit=1).id
+
+ backend_id = fields.Many2one(
+ comodel_name="prestashop.backend",
+ default=_default_backend,
+ string="Backend",
+ )
+ shop_id = fields.Many2one(
+ comodel_name="prestashop.shop",
+ default=_default_shop,
+ string="Default Shop",
+ )
+
+ def _parent_length(self, categ):
+ if not categ.parent_id:
+ return 1
+ else:
+ return 1 + self._parent_length(categ.parent_id)
+
+ def _set_main_category(self, product):
+ if product.categ_ids and product.categ_id.parent_id:
+ max_parent = {"length": 0}
+ for categ in product.categ_ids:
+ parent_length = self._parent_length(categ.parent_id)
+ if parent_length > max_parent["length"]:
+ max_parent = {"categ_id": categ.id, "length": parent_length}
+ categ_length = self._parent_length(product.categ_id.parent_id)
+ if categ_length < parent_length:
+ if product.categ_id.id not in product.categ_ids.ids:
+ product.write(
+ {
+ "categ_ids": [(4, product.categ_id.id)],
+ }
+ )
+ product.write(
+ {
+ "categ_id": max_parent["categ_id"],
+ "categ_ids": [(3, max_parent["categ_id"])],
+ }
+ )
+ else:
+ product.write(
+ {
+ "categ_id": max_parent["categ_id"],
+ "categ_ids": [(3, max_parent["categ_id"])],
+ }
+ )
+
+ def set_category(self):
+ product_obj = self.env["product.template"]
+ for product in product_obj.browse(self.env.context["active_ids"]):
+ self._set_main_category(product)
+
+ def _check_images(self, product):
+ for variant in product.product_variant_ids:
+ for image in variant.image_ids:
+ if image.owner_id != product.id:
+ image.product_id = product
+
+ def _check_category(self, product):
+ if not (product.categ_ids):
+ return False
+ return True
+
+ def _check_variants(self, product):
+ if len(product.product_variant_ids) == 1:
+ return True
+ if len(product.product_variant_ids) > 1 and not product.attribute_line_ids:
+ check_count = reduce(
+ lambda x, y: x * y,
+ [len(x.value_ids) for x in product.attribute_line_ids],
+ )
+ if check_count < len(product.product_variant_ids):
+ return False
+ return True
+
+ def export_variant_stock(self):
+ template_obj = self.env["product.template"]
+ products = template_obj.browse(self.env.context["active_ids"])
+ products.update_prestashop_quantities()
+
+ def create_prestashop_template(self, product):
+ presta_tmpl_obj = self.env["prestashop.product.template"]
+ return presta_tmpl_obj.create(
+ {
+ "backend_id": self.backend_id.id,
+ "default_shop_id": self.shop_id.id,
+ "link_rewrite": get_slug(product.name),
+ "odoo_id": product.id,
+ }
+ )
+
+ def export_products(self):
+ self.ensure_one()
+ product_obj = self.env["product.template"]
+ presta_tmpl_obj = self.env["prestashop.product.template"]
+ for product in product_obj.browse(self.env.context["active_ids"]):
+ presta_tmpl = presta_tmpl_obj.search(
+ [
+ ("odoo_id", "=", product.id),
+ ("backend_id", "=", self.backend_id.id),
+ ("default_shop_id", "=", self.shop_id.id),
+ ]
+ )
+ if not presta_tmpl:
+ self._check_images(product)
+ cat = self._check_category(product)
+ var = self._check_variants(product)
+ if not (var and cat):
+ raise ValidationError(
+ _(
+ """Product "%s" cannot be exported to Prestashop \
+because is not assigned to any Prestashop category or \
+has not any Product Variant."""
+ )
+ % product.name
+ )
+ self.create_prestashop_template(product)
+ else:
+ for tmpl in presta_tmpl:
+ if " " in tmpl.link_rewrite:
+ tmpl.link_rewrite = get_slug(tmpl.link_rewrite)
diff --git a/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml b/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
new file mode 100644
index 000000000..59bbe91f6
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
@@ -0,0 +1,63 @@
+
+
+
+ export.multiple.products.form
+ export.multiple.products
+
+
+
+
+
+ Export Products to Prestashop
+ export.multiple.products
+
+ list,form
+ form
+ new
+
+
+
+
+ export.variant.stock.form
+ export.multiple.products
+
+
+
+
+
+ Export Products Stock
+ export.multiple.products
+
+ form
+ form
+ new
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/sync_products.py b/connector_prestashop_catalog_manager/wizards/sync_products.py
new file mode 100644
index 000000000..932c3e8a4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/sync_products.py
@@ -0,0 +1,29 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+
+from odoo import models
+
+_logger = logging.getLogger(__name__)
+
+
+class SyncProducts(models.TransientModel):
+ _name = "sync.products"
+ _description = "Synchronize Products"
+
+ def _bind_resync(self, product_ids):
+ products = self.env["product.template"].browse(product_ids)
+ for product in products:
+ try:
+ for bind in product.prestashop_bind_ids:
+ bind.resync()
+ except Exception as e:
+ _logger.info("id %s, attributes %s\n", str(product.id), e)
+
+ def sync_products(self):
+ for product in self:
+ product._bind_resync(product.env.context["active_ids"])
+
+ def sync_all_products(self):
+ for product in self:
+ product._bind_resync([])
diff --git a/connector_prestashop_catalog_manager/wizards/sync_products_view.xml b/connector_prestashop_catalog_manager/wizards/sync_products_view.xml
new file mode 100644
index 000000000..c24e3f7af
--- /dev/null
+++ b/connector_prestashop_catalog_manager/wizards/sync_products_view.xml
@@ -0,0 +1,34 @@
+
+
+
+ sync.products.form
+ sync.products
+
+
+
+
+
+ Sync Products
+ sync.products
+
+ form
+ form
+ new
+
+
+