diff --git a/connector_prestashop_catalog_manager/__init__.py b/connector_prestashop_catalog_manager/__init__.py
index d549a2b6e..7588e52c8 100644
--- a/connector_prestashop_catalog_manager/__init__.py
+++ b/connector_prestashop_catalog_manager/__init__.py
@@ -1,5 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from . import consumer
from . import models
from . import wizards
diff --git a/connector_prestashop_catalog_manager/__manifest__.py b/connector_prestashop_catalog_manager/__manifest__.py
index 9913bb7ac..15143a453 100644
--- a/connector_prestashop_catalog_manager/__manifest__.py
+++ b/connector_prestashop_catalog_manager/__manifest__.py
@@ -6,9 +6,13 @@
{
"name": "Prestashop-Odoo Catalog Manager",
- "version": "9.0.1.0.2",
+ "version": "16.0.1.0.0",
"license": "AGPL-3",
- "depends": ["connector_prestashop"],
+ "depends": [
+ "connector_prestashop",
+ "product_categ_image",
+ "product_multi_image",
+ ],
"author": "Akretion,"
"AvanzOSC,"
"Tecnativa,"
@@ -24,6 +28,8 @@
"wizards/sync_products_view.xml",
"wizards/active_deactive_products_view.xml",
"views/product_image_view.xml",
+ "views/product_category_view.xml",
+ "security/ir.model.access.csv",
],
- "installable": False,
+ "installable": True,
}
diff --git a/connector_prestashop_catalog_manager/consumer.py b/connector_prestashop_catalog_manager/consumer.py
deleted file mode 100644
index 5d68862d6..000000000
--- a/connector_prestashop_catalog_manager/consumer.py
+++ /dev/null
@@ -1,295 +0,0 @@
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
-
-import re
-import unicodedata
-
-from openerp.addons.connector.connector import Binder
-from openerp.addons.connector.event import (
- on_record_create,
- on_record_unlink,
- on_record_write,
-)
-from openerp.addons.connector_prestashop.consumer import INVENTORY_FIELDS
-from openerp.addons.connector_prestashop.unit.deleter import export_delete_record
-from openerp.addons.connector_prestashop.unit.exporter import export_record
-
-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
-
-
-# TODO: attach this to a model to ease override
-CATEGORY_EXPORT_FIELDS = [
- "name",
- "parent_id",
- "description",
- "link_rewrite",
- "meta_description",
- "meta_keywords",
- "meta_title",
- "position",
-]
-
-EXCLUDE_FIELDS = ["list_price"]
-
-
-@on_record_create(model_names="prestashop.product.category")
-def prestashop_product_category_create(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="product.category")
-def product_category_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- if set(fields.keys()) <= set(CATEGORY_EXPORT_FIELDS):
- model = session.env[model_name]
- record = model.browse(record_id)
- for binding in record.prestashop_bind_ids:
- export_record.delay(
- session, binding._model._name, binding.id, fields=fields, priority=20
- )
-
-
-@on_record_write(model_names="prestashop.product.category")
-def prestashop_product_category_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- if set(fields.keys()) <= set(CATEGORY_EXPORT_FIELDS):
- export_record.delay(session, model_name, record_id, fields)
-
-
-@on_record_write(model_names="base_multi_image.image")
-def product_image_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- model = session.env[model_name]
- record = model.browse(record_id)
- for binding in record.prestashop_bind_ids:
- export_record.delay(
- session,
- "prestashop.product.image",
- binding.id,
- record.file_db_store,
- priority=20,
- )
-
-
-@on_record_unlink(model_names="base_multi_image.image")
-def product_image_unlink(session, model_name, record_id):
- if session.context.get("connector_no_export"):
- return
- model = session.env[model_name]
- record = model.browse(record_id)
- for binding in record.prestashop_bind_ids:
- backend = binding.backend_id
- product = session.env[record.owner_model].browse(record.owner_id)
- if product.exists():
- product_template = product.prestashop_bind_ids.filtered(
- lambda x: x.backend_id == binding.backend_id
- )
- if not product_template:
- return
- env_product = backend.get_environment(
- "prestashop.product.template",
- session=session,
- )
- binder_product = env_product.get_connector_unit(Binder)
- external_product_id = binder_product.to_backend(product_template.id)
-
- env = backend.get_environment(binding._name, session=session)
- binder = env.get_connector_unit(Binder)
- external_id = binder.to_backend(binding.id)
- resource = "images/products/%s" % (external_product_id)
- if external_id:
- export_delete_record.delay(
- session, binding._name, binding.backend_id.id, external_id, resource
- )
-
-
-@on_record_create(model_names="prestashop.product.template")
-def prestashop_product_template_create(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="prestashop.product.template")
-def prestashop_product_template_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- fields = list(set(fields).difference(set(INVENTORY_FIELDS)))
- if fields:
- export_record.delay(session, model_name, record_id, fields, priority=20)
- # Propagate minimal_quantity from template to variants
- if "minimal_quantity" in fields:
- ps_template = session.env[model_name].browse(record_id)
- for binding in ps_template.prestashop_bind_ids:
- binding.odoo_id.mapped("product_variant_ids.prestashop_bind_ids").write(
- {"minimal_quantity": binding.minimal_quantity}
- )
-
-
-@on_record_write(model_names="product.template")
-def product_template_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- model = session.env[model_name]
- record = model.browse(record_id)
- for binding in record.prestashop_bind_ids:
- export_record.delay(
- session,
- "prestashop.product.template",
- binding.id,
- fields,
- priority=20,
- )
-
-
-@on_record_create(model_names="prestashop.product.combination")
-def prestashop_product_combination_create(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="prestashop.product.combination")
-def prestashop_product_combination_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
- fields = list(set(fields).difference(set(INVENTORY_FIELDS)))
-
- if fields:
- export_record.delay(
- session,
- model_name,
- record_id,
- fields,
- priority=20,
- )
-
-
-def prestashop_product_combination_unlink(session, record_id):
- # binding is deactivate when deactive a product variant
- ps_binding_product = session.env["prestashop.product.combination"].search(
- [("active", "=", False), ("odoo_id", "=", record_id)]
- )
- for binding in ps_binding_product:
- resource = "combinations/%s" % (binding.prestashop_id)
- export_delete_record.delay(
- session,
- "prestashop.product.combination",
- binding.backend_id.id,
- binding.prestashop_id,
- resource,
- )
- ps_binding_product.unlink()
-
-
-@on_record_write(model_names="product.product")
-def product_product_write(session, model_name, record_id, fields):
- if session.context.get("connector_no_export"):
- return
-
- for field in EXCLUDE_FIELDS:
- fields.pop(field, None)
-
- model = session.env[model_name]
- record = model.browse(record_id)
- if not record.is_product_variant:
- return
-
- if "active" in fields and not fields["active"]:
- prestashop_product_combination_unlink(session, record_id)
- return
-
- if fields:
- for binding in record.prestashop_bind_ids:
- priority = 20
- if "default_on" in fields and fields["default_on"]:
- # PS has to uncheck actual default combination first
- priority = 99
- export_record.delay(
- session,
- "prestashop.product.combination",
- binding.id,
- fields,
- priority=priority,
- )
-
-
-@on_record_create(model_names="prestashop.product.combination.option")
-def prestashop_product_attribute_created(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_create(model_names="prestashop.product.combination.option.value")
-def prestashop_product_atrribute_value_created(
- session, model_name, record_id, fields=None
-):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="prestashop.product.combination.option")
-def prestashop_product_attribute_written(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="prestashop.product.combination.option.value")
-def prestashop_attribute_option_written(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- export_record.delay(session, model_name, record_id, priority=20)
-
-
-@on_record_write(model_names="product.attribute.value")
-def product_attribute_written(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- model = session.pool.get(model_name)
- record = model.browse(session.cr, session.uid, record_id, context=session.context)
- for binding in record.prestashop_bind_ids:
- export_record.delay(
- session,
- "prestashop.product.combination.option",
- binding.id,
- fields,
- priority=20,
- )
-
-
-@on_record_write(model_names="produc.attribute.value")
-def attribute_option_written(session, model_name, record_id, fields=None):
- if session.context.get("connector_no_export"):
- return
- model = session.pool.get(model_name)
- record = model.browse(session.cr, session.uid, record_id, context=session.context)
- for binding in record.prestashop_bind_ids:
- export_record.delay(
- session,
- "prestashop.product.combination.option.value",
- binding.id,
- fields,
- priority=20,
- )
diff --git a/connector_prestashop_catalog_manager/models/product_category/__init__.py b/connector_prestashop_catalog_manager/models/product_category/__init__.py
index 24472e23d..2c2fc9544 100644
--- a/connector_prestashop_catalog_manager/models/product_category/__init__.py
+++ b/connector_prestashop_catalog_manager/models/product_category/__init__.py
@@ -1 +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..c43456fb8
--- /dev/null
+++ b/connector_prestashop_catalog_manager/models/product_category/deleter.py
@@ -0,0 +1,12 @@
+# 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
index 10686a0b9..08c22a9fa 100644
--- a/connector_prestashop_catalog_manager/models/product_category/exporter.py
+++ b/connector_prestashop_catalog_manager/models/product_category/exporter.py
@@ -1,26 +1,20 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from openerp.addons.connector.unit.mapper import mapping
-from openerp.addons.connector_prestashop.backend import prestashop
-from openerp.addons.connector_prestashop.unit.exporter import (
- PrestashopExporter,
- export_record,
-)
-from openerp.addons.connector_prestashop.unit.mapper import (
- TranslationPrestashopExportMapper,
-)
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import changed_by, mapping
-from ...consumer import get_slug
+from ..product_template.exporter import get_slug
-@prestashop
-class ProductCategoryExporter(PrestashopExporter):
- _model_name = "prestashop.product.category"
+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.session.env["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
@@ -29,7 +23,7 @@ def _export_dependencies(self):
def export_parent_category(self, category, binder, ps_categ_obj):
if not category:
return
- ext_id = binder.to_backend(category.id, wrap=True)
+ ext_id = binder.to_external(category.id, wrap=True)
if ext_id:
return ext_id
res = {
@@ -37,21 +31,17 @@ def export_parent_category(self, category, binder, ps_categ_obj):
"odoo_id": category.id,
"link_rewrite": get_slug(category.name),
}
- category_ext_id = ps_categ_obj.with_context(connector_no_export=True).create(
- res
- )
- parent_cat_id = export_record(
- self.session, "prestashop.product.category", category_ext_id.id
- )
+ category_ext = ps_categ_obj.with_context(connector_no_export=True).create(res)
+ parent_cat_id = category_ext.export_record()
return parent_cat_id
-@prestashop
-class ProductCategoryExportMapper(TranslationPrestashopExportMapper):
- _model_name = "prestashop.product.category"
+class ProductCategoryExportMapper(Component):
+ _name = "prestashop.product.category.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.category"
direct = [
- ("sequence", "position"),
("default_shop_id", "id_shop_default"),
("active", "active"),
("position", "position"),
@@ -66,10 +56,44 @@ class ProductCategoryExportMapper(TranslationPrestashopExportMapper):
("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_backend(record.parent_id.id, wrap=True)
+ 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
index 24472e23d..2c2fc9544 100644
--- a/connector_prestashop_catalog_manager/models/product_image/__init__.py
+++ b/connector_prestashop_catalog_manager/models/product_image/__init__.py
@@ -1 +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..2c0b27e5b
--- /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()
+
+
+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
index 4be5757b4..f9bbd6a89 100644
--- a/connector_prestashop_catalog_manager/models/product_image/exporter.py
+++ b/connector_prestashop_catalog_manager/models/product_image/exporter.py
@@ -1,28 +1,19 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import os
-import os.path
-from openerp import fields, models
-from openerp.addons.connector.unit.mapper import mapping
-from openerp.addons.connector_prestashop.backend import prestashop
-from openerp.addons.connector_prestashop.unit.backend_adapter import (
+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,
)
-from openerp.addons.connector_prestashop.unit.exporter import PrestashopExporter
-from openerp.addons.connector_prestashop.unit.mapper import PrestashopExportMapper
-from openerp.tools.translate import _
-
-
-class ProductImage(models.Model):
- _inherit = "base_multi_image.image"
- front_image = fields.Boolean(string="Front image")
-
-@prestashop
-class ProductImageExport(PrestashopExporter):
- _model_name = "prestashop.product.image"
+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"""
@@ -37,20 +28,27 @@ def _run(self, fields=None):
map_record = self.mapper.map_record(self.binding)
if self.prestashop_id:
- record = map_record.values()
+ record = list(map_record.values())
if not record:
return _("Nothing to export.")
# special check on data before export
self._validate_data(record)
- self.prestashop_id = self._update(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)
- self.prestashop_id = self._create(record)
- self._after_export()
+ 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
@@ -67,55 +65,23 @@ def _link_image_to_url(self):
"type": "image/jpeg",
}
)
- if self.binding.url != full_public_url:
+ if self.binding.load_from != full_public_url:
self.binding.with_context(connector_no_export=True).write(
{
- "url": full_public_url,
- "file_db_store": False,
- "storage": "url",
+ "load_from": full_public_url,
}
)
-@prestashop
-class ProductImageExportMapper(PrestashopExportMapper):
- _model_name = "prestashop.product.image"
+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 = "%s_%s.jpg" % (
- 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))
- 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":
@@ -129,20 +95,17 @@ def product_id(self, record):
record.odoo_id.owner_id
)
binder = self.binder_for("prestashop.product.template")
- ps_product_id = binder.to_backend(product_tmpl, wrap=True)
+ 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:
- file_name = ".".join(self._get_file_name(record))
- return {"filename": file_name}
+ def load_from(self, record):
+ return {"load_from": record.load_from}
+
+ @mapping
+ def image_1920(self, record):
+ return {"image_1920": record.image_1920}
diff --git a/connector_prestashop_catalog_manager/models/product_product/__init__.py b/connector_prestashop_catalog_manager/models/product_product/__init__.py
index f43c99d95..bc9719d7f 100644
--- a/connector_prestashop_catalog_manager/models/product_product/__init__.py
+++ b/connector_prestashop_catalog_manager/models/product_product/__init__.py
@@ -1,2 +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
index 36046b9b5..ab0eb8f27 100644
--- a/connector_prestashop_catalog_manager/models/product_product/common.py
+++ b/connector_prestashop_catalog_manager/models/product_product/common.py
@@ -1,13 +1,159 @@
# © 2016 Sergio Teruel
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
-from openerp import fields, models
+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
index ab91dba05..0a16f59b8 100644
--- a/connector_prestashop_catalog_manager/models/product_product/exporter.py
+++ b/connector_prestashop_catalog_manager/models/product_product/exporter.py
@@ -1,42 +1,34 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
-from collections import OrderedDict
-
-from openerp.addons.connector.unit.mapper import mapping
-from openerp.addons.connector_prestashop.backend import prestashop
-from openerp.addons.connector_prestashop.unit.exporter import (
- PrestashopExporter,
- TranslationPrestashopExporter,
- export_record,
-)
-from openerp.addons.connector_prestashop.unit.mapper import (
- TranslationPrestashopExportMapper,
-)
+
+from odoo.addons.component.core import Component
+from odoo.addons.connector.components.mapper import changed_by, mapping
_logger = logging.getLogger(__name__)
-@prestashop
-class ProductCombinationExport(TranslationPrestashopExporter):
- _model_name = "prestashop.product.combination"
+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(ProductCombinationExport, self)._create(record)
+ 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_backend(image_line.id, wrap=True)
+ image_ext_id = image_binder.to_external(image_line.id, wrap=True)
if not image_ext_id:
- image_ext_id = (
- self.session.env["prestashop.product.image"]
+ image_ext = (
+ self.env["prestashop.product.image"]
.with_context(connector_no_export=True)
.create(
{
@@ -47,14 +39,9 @@ def _export_images(self):
.id
)
image_content = getattr(
- image_line, "_get_image_from_%s" % image_line.storage
+ image_line, "_get_image_from_%s" % image_line.load_from
)()
- export_record(
- self.session,
- "prestashop.product.image",
- image_ext_id,
- image_content,
- )
+ image_ext.export_record(image_content)
def _export_dependencies(self):
"""Export the dependencies for the product"""
@@ -63,8 +50,8 @@ def _export_dependencies(self):
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.attribute_value_ids:
- prestashop_option_id = attribute_binder.to_backend(
+ 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:
@@ -83,12 +70,10 @@ def _export_dependencies(self):
"odoo_id": value.attribute_id.id,
}
)
- export_record(
- self.session,
- "prestashop.product.combination.option",
- option_binding.id,
- )
- prestashop_value_id = option_binder.to_backend(value.id, wrap=True)
+ 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(
[
@@ -108,27 +93,24 @@ def _export_dependencies(self):
).create(
{
"backend_id": self.backend_record.id,
- "odoo_id": value.id,
+ "odoo_id": value.product_attribute_value_id.id,
"id_attribute_group": option_binding.id,
}
)
- export_record(
- self.session,
- "prestashop.product.combination.option.value",
- value_binding.id,
- )
+ value_binding.export_record()
# self._export_images()
def update_quantities(self):
- self.binding.odoo_id.with_context(self.session.context).update_prestashop_qty()
+ self.binding.odoo_id.with_context(**self.env.context).update_prestashop_qty()
def _after_export(self):
self.update_quantities()
-@prestashop
-class ProductCombinationExportMapper(TranslationPrestashopExportMapper):
- _model_name = "prestashop.product.combination"
+class ProductCombinationExportMapper(Component):
+ _name = "prestashop.product.combination.export.mapper"
+ _inherit = "translation.prestashop.export.mapper"
+ _apply_on = "prestashop.product.combination"
direct = [
("default_code", "reference"),
@@ -148,32 +130,48 @@ def combination_default(self, record):
def get_main_template_id(self, record):
template_binder = self.binder_for("prestashop.product.template")
- return template_binder.to_backend(record.main_template_id.id)
+ 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
+ )
+ tmpl_price = tmpl_prices.get(record.odoo_id.product_tmpl_id.id)
+ product_prices = pricelist._get_products_price(record.odoo_id, 1.0)
+ 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(record.impact_price / self._get_factor_tax(tax), 6)}
+ return {"price": round(extra_to_export / self._get_factor_tax(tax), 6)}
else:
- return {"price": record.impact_price}
+ return {"price": extra_to_export}
+ @changed_by("standard_price")
@mapping
def cost_price(self, record):
- return {"wholesale_price": record.standard_price}
+ wholesale_price = float(f"{record.standard_price:.2f}")
+ 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.attribute_value_ids:
- value_ext_id = option_binder.to_backend(value.id, wrap=True)
+ 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
@@ -182,43 +180,52 @@ 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_backend(image.id, wrap=True)
+ 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):
- associations = OrderedDict(
- [
- (
- "product_option_values",
- {"product_option_value": self._get_product_option_value(record)},
- ),
- ]
- )
- image = self._get_combination_image(record)
- if image:
- associations["images"] = {"image": self._get_combination_image(record)}
- return {"associations": associations}
+ 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"}
-@prestashop
-class ProductCombinationOptionExport(PrestashopExporter):
- _model_name = "prestashop.product.combination.option"
+class ProductCombinationOptionExporter(Component):
+ _name = "prestashop.product.combination.option.exporter"
+ _inherit = "prestashop.exporter"
+ _apply_on = "prestashop.product.combination.option"
def _create(self, record):
- res = super(ProductCombinationOptionExport, self)._create(record)
+ res = super()._create(record)
return res["prestashop"]["product_option"]["id"]
-@prestashop
-class ProductCombinationOptionExportMapper(TranslationPrestashopExportMapper):
- _model_name = "prestashop.product.combination.option"
+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"),
- ("group_type", "group_type"),
+ ("display_type", "group_type"),
]
_translatable_fields = [
@@ -227,32 +234,37 @@ class ProductCombinationOptionExportMapper(TranslationPrestashopExportMapper):
]
-@prestashop
-class ProductCombinationOptionValueExport(PrestashopExporter):
- _model_name = "prestashop.product.combination.option.value"
+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(ProductCombinationOptionValueExport, self)._create(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
- binder = self.binder_for("prestashop.product.combination.option")
- if not binder.to_backend(attribute_id, wrap=True):
- exporter = self.get_connector_unit_for_model(
- TranslationPrestashopExporter, "prestashop.product.combination.option"
- )
- exporter.run(attribute_id)
+ 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
-@prestashop
-class ProductCombinationOptionValueExportMapper(TranslationPrestashopExportMapper):
- _model_name = "prestashop.product.combination.option.value"
+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")]
+ direct = [
+ ("name", "value"),
+ ("prestashop_position", "position"),
+ ]
# handled by base mapping `translatable_fields`
_translatable_fields = [
("name", "name"),
@@ -264,14 +276,16 @@ def prestashop_product_attribute_id(self, record):
"prestashop.product.combination.option.value"
)
return {
- "id_feature": attribute_binder.to_backend(record.attribute_id.id, wrap=True)
+ "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_backend(
+ "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
index f43c99d95..2c2fc9544 100644
--- a/connector_prestashop_catalog_manager/models/product_template/__init__.py
+++ b/connector_prestashop_catalog_manager/models/product_template/__init__.py
@@ -1,2 +1,3 @@
-from . import exporter
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
index 8eac058bb..36b9bccce 100644
--- a/connector_prestashop_catalog_manager/models/product_template/common.py
+++ b/connector_prestashop_catalog_manager/models/product_template/common.py
@@ -1,28 +1,122 @@
# © 2016 Sergio Teruel
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
-import openerp.addons.decimal_precision as dp
-from openerp import fields, models
+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")
+ meta_title = fields.Char(translate=True)
+ meta_description = fields.Char(translate=True)
+ meta_keywords = fields.Char(translate=True)
+ tags = fields.Char(translate=True)
+ online_only = fields.Boolean()
additional_shipping_cost = fields.Float(
string="Additional Shipping Price",
- digits_compute=dp.get_precision("Product 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")
+ available_now = fields.Char(translate=True)
+ available_later = fields.Char(translate=True)
+ available_date = fields.Date()
minimal_quantity = fields.Integer(
- string="Minimal Quantity",
help="Minimal Sale quantity",
default=1,
)
+ state = fields.Boolean(default=True)
+ visibility = fields.Char(translate=True)
+ low_stock_threshold = fields.Integer(
+ help="Low Stock Threshold",
+ default=0,
+ )
+ low_stock_alert = fields.Integer(
+ 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
index 4210f9c98..04490516a 100644
--- a/connector_prestashop_catalog_manager/models/product_template/exporter.py
+++ b/connector_prestashop_catalog_manager/models/product_template/exporter.py
@@ -1,36 +1,49 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import logging
+import re
+import unicodedata
from datetime import timedelta
-from openerp.addons.connector.unit.mapper import m2o_to_backend, mapping
-from openerp.addons.connector_prestashop.backend import prestashop
-from openerp.addons.connector_prestashop.models.product_template.importer import (
- ProductTemplateImporter,
-)
-from openerp.addons.connector_prestashop.unit.exporter import (
- TranslationPrestashopExporter,
- export_record,
-)
-from openerp.addons.connector_prestashop.unit.mapper import (
- TranslationPrestashopExportMapper,
-)
+from odoo import fields
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
-from ...consumer import get_slug
+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
-@prestashop
-class ProductTemplateExporter(TranslationPrestashopExporter):
- _model_name = "prestashop.product.template"
+_logger = logging.getLogger(__name__)
+
+
+def get_slug(name):
+ if slugify_lib:
+ try:
+ return slugify_lib.slugify(name)
+ except TypeError as e:
+ _logger.info("get_slug TypeError: %s", e)
+ 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(ProductTemplateExporter, self)._create(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.export_variants()
self.check_images()
self.backend_adapter.write(self.prestashop_id, data)
@@ -39,9 +52,9 @@ def write_binging_vals(self, erp_record, ps_record):
("description_short_html", "description_short"),
("description_html", "description"),
]
- trans = ProductTemplateImporter(self.connector_env)
+ trans = self.component(usage="record.importer")
splitted_record = trans._split_per_language(ps_record)
- for lang_code, prestashop_record in splitted_record.items():
+ for lang_code, prestashop_record in list(splitted_record.items()):
vals = {}
for key in keys_to_update:
vals[key[0]] = prestashop_record[key[1]]
@@ -53,11 +66,11 @@ def export_categories(self, category):
if not category:
return
category_binder = self.binder_for("prestashop.product.category")
- ext_id = category_binder.to_backend(category.id, wrap=True)
+ ext_id = category_binder.to_external(category, wrap=True)
if ext_id:
return ext_id
- ps_categ_obj = self.session.env["prestashop.product.category"]
+ 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 = {
@@ -67,7 +80,7 @@ def export_categories(self, category):
"position": obj_position,
}
binding = ps_categ_obj.with_context(connector_no_export=True).create(res)
- export_record(self.session, "prestashop.product.category", binding.id)
+ binding.export_record()
def _parent_length(self, categ):
if not categ.parent_id:
@@ -77,7 +90,7 @@ def _parent_length(self, categ):
def _export_dependencies(self):
"""Export the dependencies for the product"""
- super(ProductTemplateExporter, self)._export_dependencies()
+ res = super()._export_dependencies()
attribute_binder = self.binder_for("prestashop.product.combination.option")
option_binder = self.binder_for("prestashop.product.combination.option.value")
@@ -85,33 +98,34 @@ def _export_dependencies(self):
self.export_categories(category)
for line in self.binding.attribute_line_ids:
- attribute_ext_id = attribute_binder.to_backend(
- line.attribute_id.id, wrap=True
+ 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_backend(value.id, wrap=True)
+ value_ext_id = option_binder.to_external(value, wrap=True)
if not value_ext_id:
self._export_dependency(
value, "prestashop.product.combination.option.value"
)
+ return res
def export_variants(self):
- combination_obj = self.session.env["prestashop.product.combination"]
+ combination_obj = self.env["prestashop.product.combination"]
for product in self.binding.product_variant_ids:
- if not product.attribute_value_ids:
+ if not product.product_template_attribute_value_ids:
continue
- combination_ext_id = combination_obj.search(
+ combination_ext = combination_obj.search(
[
("backend_id", "=", self.backend_record.id),
("odoo_id", "=", product.id),
]
)
- if not combination_ext_id:
- combination_ext_id = combination_obj.with_context(
+ if not combination_ext:
+ combination_ext = combination_obj.with_context(
connector_no_export=True
).create(
{
@@ -122,13 +136,9 @@ def export_variants(self):
)
# If a template has been modified then always update PrestaShop
# combinations
- export_record.delay(
- self.session,
- "prestashop.product.combination",
- combination_ext_id.id,
- priority=50,
- eta=timedelta(seconds=20),
- )
+ combination_ext.with_delay(
+ priority=50, eta=timedelta(seconds=20)
+ ).export_record()
def _not_in_variant_images(self, image):
images = []
@@ -141,7 +151,7 @@ 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_backend(image.id, wrap=True)
+ 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
@@ -149,8 +159,8 @@ def check_images(self):
# ValueError:
# Expected singleton: prestashop.product.image(x, y)
if image_ext_id is None:
- image_ext_id = (
- self.session.env["prestashop.product.image"]
+ image_ext = (
+ self.env["prestashop.product.image"]
.with_context(connector_no_export=True)
.create(
{
@@ -159,12 +169,7 @@ def check_images(self):
}
)
)
- export_record.delay(
- self.session,
- "prestashop.product.image",
- image_ext_id.id,
- priority=15,
- )
+ image_ext.with_delay(priority=5).export_record()
def update_quantities(self):
if len(self.binding.product_variant_ids) == 1:
@@ -175,30 +180,40 @@ 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()
-@prestashop
-class ProductTemplateExportMapper(TranslationPrestashopExportMapper):
- _model_name = "prestashop.product.template"
+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_backend("default_shop_id"), "id_shop_default"),
+ (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_backend(
+ 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"),
+ ("visibility", "visibility"),
]
# handled by base mapping `translatable_fields`
_translatable_fields = [
@@ -217,81 +232,89 @@ class ProductTemplateExportMapper(TranslationPrestashopExportMapper):
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)
+ 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(record.list_price / self._get_factor_tax(tax), 6))
- }
+ return {"price": str(round(price_to_export / self._get_factor_tax(tax), 6))}
else:
- return {"price": str(record.list_price)}
+ return {"price": str(price_to_export)}
+ @changed_by("standard_price")
@mapping
- def reference(self, record):
- return {"reference": record.reference or record.default_code or ""}
+ def cost_price(self, record):
+ wholesale_price = float(f"{record.standard_price:.2f}")
+ return {"wholesale_price": wholesale_price}
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_backend(category.id, wrap=True)})
+ 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_backend(record.taxes_id[:1].tax_group_id, wrap=True)
+ 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}
+ 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}
+ 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_backend(default_image, wrap=True)
+ ps_image_id = binder.to_external(default_image, wrap=True)
if ps_image_id:
return {"id_default_image": ps_image_id}
@mapping
- def extras_manufacturer(self, record):
- mapper = self.unit_for(ManufacturerExportMapper)
- return mapper.map_record(record).values(**self.options)
-
-
-@prestashop
-class ManufacturerExportMapper(TranslationPrestashopExportMapper):
- # To extend in connector_prestashop_manufacturer module
- _model_name = "prestashop.product.template"
-
- _translatable_fields = [
- ("name", "name"),
- ]
-
- @mapping
- def manufacturer(self, record):
- return {}
+ 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/security/ir.model.access.csv b/connector_prestashop_catalog_manager/security/ir.model.access.csv
new file mode 100644
index 000000000..bc98bca0d
--- /dev/null
+++ b/connector_prestashop_catalog_manager/security/ir.model.access.csv
@@ -0,0 +1,7 @@
+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
diff --git a/connector_prestashop_catalog_manager/tests/__init__.py b/connector_prestashop_catalog_manager/tests/__init__.py
new file mode 100644
index 000000000..130e754a4
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/__init__.py
@@ -0,0 +1,5 @@
+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
diff --git a/connector_prestashop_catalog_manager/tests/common.py b/connector_prestashop_catalog_manager/tests/common.py
new file mode 100644
index 000000000..6de40552f
--- /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")
+
+ def tearDown(self):
+ super().tearDown()
+ self.patch_delay_record.stop()
diff --git a/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_attribute.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_attribute.yaml
new file mode 100644
index 000000000..63f824e6f
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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/test_export_product_attribute_value.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_attribute_value.yaml
new file mode 100644
index 000000000..7d91119b7
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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/test_export_product_category.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_category.yaml
new file mode 100644
index 000000000..29183ec08
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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/test_export_product_image.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_image.yaml
new file mode 100644
index 000000000..8c8b5f16f
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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/test_export_product_product.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_product.yaml
new file mode 100644
index 000000000..42bfa2ddc
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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/test_export_product_template.yaml b/connector_prestashop_catalog_manager/tests/fixtures/test_export_product_template.yaml
new file mode 100644
index 000000000..eb9b33fcb
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/fixtures/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..6726aa9ce
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_attribute.py
@@ -0,0 +1,192 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+from odoo.tests import tagged
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+@tagged("post_install", "-at_install")
+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_01_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_02_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_03_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_04_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_05_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_06_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_category.py b/connector_prestashop_catalog_manager/tests/test_export_product_category.py
new file mode 100644
index 000000000..dcb8de17a
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_category.py
@@ -0,0 +1,126 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+from odoo.tests import tagged
+
+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
+
+
+@tagged("post_install", "-at_install")
+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_01_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_02_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_03_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..94683c4e9
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_image.py
@@ -0,0 +1,137 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+from odoo.tests import tagged
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+@tagged("post_install", "-at_install")
+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",
+ }
+ )
+ self.transparent_image = ( # 1x1 Transparent GIF
+ b"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
+ )
+ self.grey_image = ( # 1x1 Grey GIF
+ b"R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw =="
+ )
+ # create image and binding
+ self.image = self.env["base_multi_image.image"].create(
+ {
+ "owner_id": template.id,
+ "owner_model": "product.template",
+ "image_1920": self.transparent_image,
+ }
+ )
+ self.image._onchange_load_from()
+ self.binding = self.create_binding_no_export(
+ "prestashop.product.image", self.image.id, None
+ )
+
+ @assert_no_job_delayed
+ def test_01_export_product_image_onwrite(self):
+ # write in image
+ self.image.write(
+ {
+ "image_1920": self.grey_image,
+ }
+ )
+ # check export delayed
+ self.instance_delay_record.export_record.assert_called_once_with(
+ fields=[
+ "load_from",
+ ]
+ )
+
+ @assert_no_job_delayed
+ def test_02_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_03_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({"image_1920": self.grey_image})
+
+ # 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..553a7483d
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_product.py
@@ -0,0 +1,231 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from unittest import mock
+
+from odoo.tests import tagged
+
+from odoo.addons.connector_prestashop.tests.common import (
+ assert_no_job_delayed,
+ recorder,
+)
+
+from .common import CatalogManagerTransactionCase
+
+
+@tagged("post_install", "-at_install")
+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_01_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_02_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_03_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_04_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(
+ f"/api/combinations/{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..198f97aa5
--- /dev/null
+++ b/connector_prestashop_catalog_manager/tests/test_export_product_template.py
@@ -0,0 +1,261 @@
+# © 2018 PlanetaTIC
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+
+from odoo.tests import tagged
+
+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
+
+
+@tagged("post_install", "-at_install")
+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
+ )
+
+ # 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,
+ }
+ )
+
+ 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_01_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_02_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_03_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_04_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_05_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
index b05f0783e..cdd4f03ca 100644
--- a/connector_prestashop_catalog_manager/views/product_attribute_view.xml
+++ b/connector_prestashop_catalog_manager/views/product_attribute_view.xml
@@ -1,46 +1,41 @@
-
-
-
+
+
+
+ product.attribute.form
+ product.attribute
+
+
+
+
-
-
-
- product.attribute.form
- product.attribute
-
+
+ prestashop.product.combination.option
+
-
-
+
+
-
- prestashop.product.combination.option
-
-
-
-
-
-
- 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..62f69711a
--- /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
index bbefd0109..e1443ff79 100644
--- a/connector_prestashop_catalog_manager/views/product_image_view.xml
+++ b/connector_prestashop_catalog_manager/views/product_image_view.xml
@@ -1,15 +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
index 27995ef8f..ca8d5d131 100644
--- a/connector_prestashop_catalog_manager/views/product_view.xml
+++ b/connector_prestashop_catalog_manager/views/product_view.xml
@@ -1,31 +1,46 @@
-
-
-
- connector_prestashop.product.template.form
-
- prestashop.product.template
-
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ 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/active_deactive_products.py b/connector_prestashop_catalog_manager/wizards/active_deactive_products.py
index c4d935ad5..5a6458537 100644
--- a/connector_prestashop_catalog_manager/wizards/active_deactive_products.py
+++ b/connector_prestashop_catalog_manager/wizards/active_deactive_products.py
@@ -1,13 +1,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from openerp import api, fields, models
+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",
)
@@ -19,10 +19,10 @@ def _change_status(self, status):
if bind.always_available != status or self.force_status:
bind.always_available = status
- @api.multi
def active_products(self):
- self._change_status(True)
+ for product in self:
+ product._change_status(True)
- @api.multi
def deactive_products(self):
- self._change_status(False)
+ 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
index 7bd42b0ad..4da7b2838 100644
--- a/connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml
+++ b/connector_prestashop_catalog_manager/wizards/active_deactive_products_view.xml
@@ -1,40 +1,40 @@
-
-
+
active.product.form
active.deactive.products
-
+
+ Active Products
+ active.deactive.products
+
+ form
+ form
+ new
+
+
deactive.product.form
@@ -42,30 +42,31 @@
-
-
-
+
+ Deactive Products
+ active.deactive.products
+
+ form
+ form
+ new
+
+
+
+
diff --git a/connector_prestashop_catalog_manager/wizards/export_category.py b/connector_prestashop_catalog_manager/wizards/export_category.py
index e6fac1114..bde658155 100644
--- a/connector_prestashop_catalog_manager/wizards/export_category.py
+++ b/connector_prestashop_catalog_manager/wizards/export_category.py
@@ -1,30 +1,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import re
-import unicodedata
+from odoo import fields, models
-from openerp import api, fields, models
-
-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
+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
@@ -43,7 +26,6 @@ def _default_shop(self):
string="Shop",
)
- @api.multi
def export_categories(self):
self.ensure_one()
category_obj = self.env["product.category"]
diff --git a/connector_prestashop_catalog_manager/wizards/export_category_view.xml b/connector_prestashop_catalog_manager/wizards/export_category_view.xml
index 79d8286a0..d45d45375 100644
--- a/connector_prestashop_catalog_manager/wizards/export_category_view.xml
+++ b/connector_prestashop_catalog_manager/wizards/export_category_view.xml
@@ -1,6 +1,5 @@
-
-
+
wiz.prestashop.export.category.form
@@ -11,30 +10,26 @@
-
+
-
+
+ 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
index db41ba98f..b0eda5702 100644
--- a/connector_prestashop_catalog_manager/wizards/export_multiple_products.py
+++ b/connector_prestashop_catalog_manager/wizards/export_multiple_products.py
@@ -1,9 +1,15 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import logging
import re
import unicodedata
+from functools import reduce
+
+from odoo import _, fields, models
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
-from openerp import api, fields, models
try:
import slugify as slugify_lib
@@ -15,8 +21,8 @@ def get_slug(name):
if slugify_lib:
try:
return slugify_lib.slugify(name)
- except TypeError:
- pass
+ except TypeError as e:
+ _logger.info("get_slug TypeError: %s", e)
uni = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
slug = re.sub(r"[\W_]", " ", uni).strip().lower()
slug = re.sub(r"[-\s]+", "-", slug)
@@ -25,12 +31,11 @@ def get_slug(name):
class ExportMultipleProducts(models.TransientModel):
_name = "export.multiple.products"
+ _description = "Export Multiple Products"
- @api.multi
def _default_backend(self):
return self.env["prestashop.backend"].search([], limit=1).id
- @api.multi
def _default_shop(self):
return self.env["prestashop.shop"].search([], limit=1).id
@@ -80,7 +85,6 @@ def _set_main_category(self, product):
}
)
- @api.multi
def set_category(self):
product_obj = self.env["product.template"]
for product in product_obj.browse(self.env.context["active_ids"]):
@@ -103,19 +107,17 @@ def _check_variants(self, product):
if len(product.product_variant_ids) > 1 and not product.attribute_line_ids:
check_count = reduce(
lambda x, y: x * y,
- map(lambda x: len(x.value_ids), product.attribute_line_ids),
+ [len(x.value_ids) for x in product.attribute_line_ids],
)
if check_count < len(product.product_variant_ids):
return False
return True
- @api.multi
def export_variant_stock(self):
template_obj = self.env["product.template"]
products = template_obj.browse(self.env.context["active_ids"])
products.update_prestashop_quantities()
- @api.multi
def create_prestashop_template(self, product):
presta_tmpl_obj = self.env["prestashop.product.template"]
return presta_tmpl_obj.create(
@@ -127,7 +129,6 @@ def create_prestashop_template(self, product):
}
)
- @api.multi
def export_products(self):
self.ensure_one()
product_obj = self.env["product.template"]
@@ -145,7 +146,14 @@ def export_products(self):
cat = self._check_category(product)
var = self._check_variants(product)
if not (var and cat):
- continue
+ 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:
diff --git a/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml b/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
index aa4a1b787..59bbe91f6 100644
--- a/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
+++ b/connector_prestashop_catalog_manager/wizards/export_multiple_products_view.xml
@@ -1,70 +1,63 @@
-
-
-
- export.multiple.products.form
- export.multiple.products
-
-
-
-
-
+
+
+ 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.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
index f306bafbd..932c3e8a4 100644
--- a/connector_prestashop_catalog_manager/wizards/sync_products.py
+++ b/connector_prestashop_catalog_manager/wizards/sync_products.py
@@ -2,27 +2,28 @@
import logging
-from openerp import api, models
+from odoo import models
_logger = logging.getLogger(__name__)
class SyncProducts(models.TransientModel):
- _name = 'sync.products'
+ _name = "sync.products"
+ _description = "Synchronize Products"
def _bind_resync(self, product_ids):
- products = self.env['product.template'].browse(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, e:
- _logger.info('id %s, attributes %s\n', str(product.id), e)
+ except Exception as e:
+ _logger.info("id %s, attributes %s\n", str(product.id), e)
- @api.multi
def sync_products(self):
- self._bind_resync(self.env.context['active_ids'])
+ for product in self:
+ product._bind_resync(product.env.context["active_ids"])
- @api.multi
def sync_all_products(self):
- self._bind_resync([])
+ 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
index 03b322dae..c24e3f7af 100644
--- a/connector_prestashop_catalog_manager/wizards/sync_products_view.xml
+++ b/connector_prestashop_catalog_manager/wizards/sync_products_view.xml
@@ -1,35 +1,34 @@
-
-
-
- sync.products.form
- sync.products
-
-
-
-
-
-
-
+
+
+ sync.products.form
+ sync.products
+
+
+
+
+
+ Sync Products
+ sync.products
+
+ form
+ form
+ new
+
+
+