diff --git a/connector_magento/models/product/importer.py b/connector_magento/models/product/importer.py index 937605337..3887087f3 100644 --- a/connector_magento/models/product/importer.py +++ b/connector_magento/models/product/importer.py @@ -118,6 +118,43 @@ def run(self, external_id, binding): self._write_image_data(binding, binary, image_data) +# class ConfigurableImporter(Component): +# """ Can be inherited to change the way the configurable products are +# imported. +# +# called at the end of the import of a product. +# +# By default, the configurable products are imported as simple products, the +# configurable product is skipped and his children are imported separately. +# +# If you want to create a custom importer for the configurables, you have to +# inherit the Component:: +# +# class ConfigurableImporter(Component): +# _inherit = 'magento.product.configurable.importer' +# +# And to bypass the _must_skip:: +# +# class ProductImporter(Component): +# _inherit = 'magento.product.product.importer' +# +# def _must_skip(self): +# res = super(ProductImporter, self)._must_skip() +# if self.magento_record['type_id'] != 'configurable': +# return res +# """ +# _name = 'magento.product.configurable.importer' +# _inherit = 'magento.importer' +# _apply_on = ['magento.product.product'] +# _usage = 'product.configurable.importer' +# +# def run(self, binding, magento_record): +# """ import the configurable information about a product. +# +# :param magento_record: product information from Magento +# """ + + # TODO: not needed, use inheritance class BundleImporter(Component): """ Can be inherited to change the way the bundle products are diff --git a/connector_magento_configurable/README.rst b/connector_magento_configurable/README.rst new file mode 100644 index 000000000..281eee12d --- /dev/null +++ b/connector_magento_configurable/README.rst @@ -0,0 +1,37 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================================================== +Magento Connector - Configurable Product +==================================================== + +This modules aims to create configurable products as Product templates and +linked ProductProduct and not flat ProductProduct + +Installation +============ + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + + +Maintainer +---------- + diff --git a/connector_magento_configurable/__init__.py b/connector_magento_configurable/__init__.py new file mode 100644 index 000000000..a0fdc10fe --- /dev/null +++ b/connector_magento_configurable/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/connector_magento_configurable/__manifest__.py b/connector_magento_configurable/__manifest__.py new file mode 100644 index 000000000..c11409e0b --- /dev/null +++ b/connector_magento_configurable/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{'name': 'Magento Connector - Configurable', + 'version': '10.0.1.0.0', + 'author': 'Akretion, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'category': 'Hidden', + 'depends': ['connector_magento'], + 'data': [ + # 'security/ir.model.access.csv', + 'views/magento_backend_views.xml', + ], + 'installable': True, + 'auto_install': True, + } diff --git a/connector_magento_configurable/models/__init__.py b/connector_magento_configurable/models/__init__.py new file mode 100644 index 000000000..cb9421a1b --- /dev/null +++ b/connector_magento_configurable/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from . import configurable +from . import inherits +from . import product_attribute +from . import product_attribute_value +from . import product_attribute_line diff --git a/connector_magento_configurable/models/configurable.py b/connector_magento_configurable/models/configurable.py new file mode 100644 index 000000000..8d4048aee --- /dev/null +++ b/connector_magento_configurable/models/configurable.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging + +from odoo import models + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class ConfigurableBatchImporter(Component): + """ Import the Configurable Products. + + For every Configurable Product not yet converted from flat + to templated product, creates a job + """ + _name = 'magento.product.configurable.batch.importer' + _inherit = 'magento.delayed.batch.importer' + _apply_on = ['magento.product.configurable'] + + def run(self, filters=None): + """ Run the synchronization """ + # from_date = filters.pop('from_date', None) + # to_date = filters.pop('to_date', None) + internal_ids = self.env['magento.product.product'].search( + [('product_type', '=', 'configurable')] + ) + _logger.info('search for configurable products %s returned %s', + filters, internal_ids) + for internal_id in internal_ids: + self._import_record(internal_id) + + +class ConfigurableImporter(Component): + _name = 'magento.product.configurable.importer' + _inherit = 'magento.importer' + _apply_on = ['magento.product.configurable'] + + def run(self, record, force=False): + filters = {'record': record} + self.env['magento.product.attribute'].import_batch( + self.backend_record, + filters, + ) + self.env['magento.product.attribute.line'].import_batch( + self.backend_record, + filters, + ) + + +class MagentoProductConfigurable(models.Model): + _name = 'magento.product.configurable' + _inherit = 'magento.binding' + _inherits = {'product.product': 'odoo_id'} + _description = 'Magento Product Configurable' diff --git a/connector_magento_configurable/models/inherits.py b/connector_magento_configurable/models/inherits.py new file mode 100644 index 000000000..320129a9c --- /dev/null +++ b/connector_magento_configurable/models/inherits.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import models, fields, api + +from odoo.addons.component.core import Component + + +class MagentoBackend(models.Model): + _inherit = 'magento.backend' + + import_configurables_from_date = fields.Datetime( + string='Import configurables from date', + ) + + @api.multi + def import_product_configurable(self): + self._import_from_date('magento.product.configurable', + 'import_configurables_from_date') + return True + + @api.model + def _scheduler_import_product_configurable(self, domain=None): + self._magento_backend('import_product_configurable', domain=domain) + + +class ProductImporter(Component): + _inherit = 'magento.product.product.importer' + + """ + Returns None if the product_type is configurable + So that it is not skipped + """ + def _must_skip(self): + res = super(ProductImporter, self)._must_skip() + if self.magento_record['type_id'] != 'configurable': + return res + + +class MagentoProductProduct(models.Model): + _inherit = 'magento.product.product' + + transformed_at = fields.Date( + 'Transformed At (from simple to templated product)' + ) + + +class MagentoConfigurableModelBinder(Component): + _name = 'magento.configurable.binder' + _inherit = 'magento.binder' + _apply_on = [ + 'magento.product.attribute', + 'magento.product.attribute.value', + 'magento.product.attribute.line', + ] diff --git a/connector_magento_configurable/models/product_attribute/__init__.py b/connector_magento_configurable/models/product_attribute/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_magento_configurable/models/product_attribute/common.py b/connector_magento_configurable/models/product_attribute/common.py new file mode 100644 index 000000000..e624aedab --- /dev/null +++ b/connector_magento_configurable/models/product_attribute/common.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import xmlrpclib +from odoo import models, fields +from odoo.addons.connector.exception import IDMissingInBackend +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class MagentoProductAttribute(models.Model): + _name = 'magento.product.attribute' + _inherit = 'magento.binding' + _inherits = {'product.attribute': 'odoo_id'} + _description = 'Magento Product Attribute' + + odoo_id = fields.Many2one( + comodel_name='product.attribute', + string='Product Attribute', + required=True, + ondelete='cascade') + + +class ProductAttribute(models.Model): + _inherit = 'product.attribute' + + magento_bind_ids = fields.One2many( + comodel_name='magento.product.attribute', + inverse_name='odoo_id', + string="Magento Bindings", + ) + + +class ProductAttributeAdapter(Component): + _name = 'magento.product.attribute.adapter' + _inherit = 'magento.adapter' + _apply_on = 'magento.product.attribute' + + _magento_model = 'ol_catalog_product_link' + _admin_path = '/{model}/index/' + + def _call(self, method, arguments): + try: + return super(ProductAttributeAdapter, self)._call( + method, + arguments) + except xmlrpclib.Fault as err: + # 101 is the error in the Magento API + # when the attribute does not exist + if err.faultCode == 101: + raise IDMissingInBackend + else: + raise + + def list_attributes(self, sku, storeview_id=None, attributes=None): + """Returns the list of the super attributes and their possible values + from a specific configurable product + + :rtype: dict + """ + return self._call('%s.listSuperAttributes' % self._magento_model, + [sku, storeview_id, attributes]) diff --git a/connector_magento_configurable/models/product_attribute/importer.py b/connector_magento_configurable/models/product_attribute/importer.py new file mode 100644 index 000000000..405ea330a --- /dev/null +++ b/connector_magento_configurable/models/product_attribute/importer.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping + + +class ProductAttributeBatchImporter(Component): + """ Import the Magento Product Attributes. + """ + _name = 'magento.product.attribute.batch.importer' + _inherit = 'magento.delayed.batch.importer' + _apply_on = ['magento.product.attribute'] + + def run(self, filters=None): + """ Run the synchronization """ + record = filters['record'] + updated_attributes = self.backend_adapter.list_attributes( + record.default_code) + for attribute in updated_attributes: + self._import_record(attribute) + + +class ProductAttributeImporter(Component): + _name = 'magento.product.attribute.importer' + _inherit = 'magento.importer' + _apply_on = ['magento.product.attribute'] + + def _get_magento_data(self, storeview_id=None): + """ + In this case, + the magento_record already contains all the data to insert, + no need to make a xmlrpc call + """ + return self.magento_record + + def _after_import(self, binding): + self.env['magento.product.attribute.value'].import_batch( + self.backend_record, + { + 'values': self.magento_record['values'], + 'attribute_id': self.magento_record['attribute_id'], + } + ) + + def run(self, magento_record, force=False): + self.magento_record = magento_record + super(ProductAttributeImporter, self).run( + magento_record['attribute_id'], + force, + ) + + +class ProductAttributeImportMapper(Component): + _name = 'magento.product.attribute.import.mapper' + _inherit = 'magento.import.mapper' + _apply_on = 'magento.product.attribute' + + direct = [ + ('attribute_code', 'name'), + ('attribute_id', 'external_id'), + ('store_label', 'display_name'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} diff --git a/connector_magento_configurable/models/product_attribute_line/__init__.py b/connector_magento_configurable/models/product_attribute_line/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_line/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_magento_configurable/models/product_attribute_line/common.py b/connector_magento_configurable/models/product_attribute_line/common.py new file mode 100644 index 000000000..4b4944f06 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_line/common.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import xmlrpclib +from odoo import models, fields +from odoo.addons.connector.exception import IDMissingInBackend +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class MagentoProductAttributeLine(models.Model): + _name = 'magento.product.attribute.line' + _inherit = 'magento.binding' + _inherits = {'product.attribute.line': 'odoo_id'} + _description = 'Magento Product Attribute Line' + + odoo_id = fields.Many2one( + comodel_name='product.attribute.line', + string='Product Attribute Line', + required=True, + ondelete='cascade') + + +class ProductAttributeLine(models.Model): + _inherit = 'product.attribute.line' + + magento_bind_ids = fields.One2many( + comodel_name='magento.product.attribute.line', + inverse_name='odoo_id', + string="Magento Bindings", + ) + + +class ProductAttributeLineAdapter(Component): + _name = 'magento.product.attribute.line.adapter' + _inherit = 'magento.adapter' + _apply_on = 'magento.product.attribute.line' + + _magento_model = 'ol_catalog_product_link' + _admin_path = '/{model}/index/' + + def _call(self, method, arguments): + try: + return super(ProductAttributeLineAdapter, self)._call( + method, + arguments) + except xmlrpclib.Fault as err: + # 101 is the error in the Magento API + # when the attribute does not exist + if err.faultCode == 101: + raise IDMissingInBackend + else: + raise + + def list_variants(self, sku, storeview_id=None, attributes=None): + """ Returns the information of a record + + :rtype: dict + """ + return self._call('%s.list' % self._magento_model, + [sku, storeview_id, attributes]) diff --git a/connector_magento_configurable/models/product_attribute_line/importer.py b/connector_magento_configurable/models/product_attribute_line/importer.py new file mode 100644 index 000000000..d22fc5655 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_line/importer.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping +from odoo.addons.connector.exception import MappingError + + +class ProductAttributeLineBatchImporter(Component): + """ Import the Magento Product Attribute Lines. + """ + _name = 'magento.product.attribute.line.batch.importer' + _inherit = 'magento.delayed.batch.importer' + _apply_on = ['magento.product.attribute.line'] + + def _write_product(self, magento_product, tmpl_id, value_ids): + magento_product.write( + {'product_tmpl_id': tmpl_id, + 'attribute_value_ids': value_ids}) + magento_product.odoo_id.write( + {'product_tmpl_id': tmpl_id, + 'attribute_value_ids': value_ids}) + + def _get_magento_product_attribute_line(self, + attribute, value, magento_product): + line = {} + line['attribute_id'] = attribute['odoo_id'][0] + line['value_ids'] = [(4, value.odoo_id.id)] + line['template_id'] = magento_product.odoo_id.product_tmpl_id.id + line['attribute_name'] = attribute['name'] + line['external_id'] = str(line['template_id']) + line['external_id'] += '_' + line['external_id'] += line['attribute_name'] + return line + + def _import_magento_product_attribute_line(self, + record, variant, + attribute, value): + line = self._get_magento_product_attribute_line( + attribute, + value, + record + ) + self._import_record(line) + + def run(self, filters=None): + """ Run the synchronization """ + record = filters['record'] + updated_variants = self.backend_adapter.list_variants( + record.default_code) + available_attributes = self.env[ + 'magento.product.attribute'].search_read([], [ + 'name', + 'odoo_id', + ]) + value_binder = self.binder_for('magento.product.attribute.value') + for variant in updated_variants: + magento_product = self.env['magento.product.product'].search([ + ('external_id', '=', variant['entity_id']) + ]) + attribute_value_ids = [] + for attribute in available_attributes: + if variant.get(attribute['name']): + value = value_binder.to_internal( + variant[attribute['name']], unwrap=False) + if not value: + raise MappingError("The product attribute value with " + "magento id %s is not imported." % + variant[attribute['name']]) + self._import_magento_product_attribute_line( + record, + variant, + attribute, + value, + ) + attribute_value_ids.append((4, value.odoo_id.id)) + if attribute_value_ids: + self._write_product( + magento_product, + record.product_tmpl_id.id, + attribute_value_ids, + ) + + +class ProductAttributeLineImporter(Component): + _name = 'magento.product.attribute.line.importer' + _inherit = 'magento.importer' + _apply_on = ['magento.product.attribute.line'] + + def _get_magento_data(self, storeview_id=None): + """ + In this case, + the magento_record already contains all the data to insert, + no need to make a xmlrpc call + """ + return self.magento_record + + def run(self, magento_record, force=False): + self.magento_record = magento_record + super(ProductAttributeLineImporter, self).run( + magento_record['external_id'], + force, + ) + + +class ProductAttributeLineImportMapper(Component): + _name = 'magento.product.attribute.line.import.mapper' + _inherit = 'magento.import.mapper' + _apply_on = 'magento.product.attribute.line' + + direct = [ + ('value_ids', 'value_ids'), + ] + + @mapping + def product_tmpl_id(self, record): + return {'product_tmpl_id': record['template_id']} + + @mapping + def attribute_id(self, record): + return {'attribute_id': record['attribute_id']} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} diff --git a/connector_magento_configurable/models/product_attribute_value/__init__.py b/connector_magento_configurable/models/product_attribute_value/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_value/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_magento_configurable/models/product_attribute_value/common.py b/connector_magento_configurable/models/product_attribute_value/common.py new file mode 100644 index 000000000..ead77b035 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_value/common.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import models, fields + +_logger = logging.getLogger(__name__) + + +class MagentoProductAttributeValue(models.Model): + _name = 'magento.product.attribute.value' + _inherit = 'magento.binding' + _inherits = {'product.attribute.value': 'odoo_id'} + _description = 'Magento Product Attribute' + + odoo_id = fields.Many2one( + comodel_name='product.attribute.value', + string='Product Attribute', + required=True, + ondelete='cascade') + + magento_attribute_id = fields.Many2one( + comodel_name='magento.product.attribute', + string='Magento Attribute', + ondelete='cascade', + ) + + +class ProductAttributeValue(models.Model): + _inherit = 'product.attribute.value' + + magento_bind_ids = fields.One2many( + comodel_name='magento.product.attribute.value', + inverse_name='odoo_id', + string="Magento Bindings", + ) diff --git a/connector_magento_configurable/models/product_attribute_value/importer.py b/connector_magento_configurable/models/product_attribute_value/importer.py new file mode 100644 index 000000000..a64a30594 --- /dev/null +++ b/connector_magento_configurable/models/product_attribute_value/importer.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping +from odoo.addons.connector.exception import MappingError + + +class ProductAttributeValueBatchImporter(Component): + """ Import the Magento Product Attributes. + """ + _name = 'magento.product.attribute.value.batch.importer' + _inherit = 'magento.delayed.batch.importer' + _apply_on = ['magento.product.attribute.value'] + + def run(self, filters=None): + """ Run the synchronization """ + for value in filters['values']: + value['attribute_id'] = filters['attribute_id'] + self._import_record(value) + + +class ProductAttributeValueImporter(Component): + _name = 'magento.product.attribute.value.importer' + _inherit = 'magento.importer' + _apply_on = ['magento.product.attribute.value'] + + def _get_magento_data(self, storeview_id=None): + """ + In this case, the magento_record contains all the data to insert + """ + return self.magento_record + + def run(self, magento_record, force=False): + self.magento_record = magento_record + super(ProductAttributeValueImporter, self).run( + magento_record['value_index'], + force, + ) + + +class ProductAttributeValueImportMapper(Component): + _name = 'magento.product.attribute.value.import.mapper' + _inherit = 'magento.import.mapper' + _apply_on = 'magento.product.attribute.value' + + direct = [ + ('label', 'name'), + ('value_index', 'external_id'), + ('store_label', 'display_name'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + @mapping + def attribute_id(self, record): + if not record.get('attribute_id'): + return + binder = self.binder_for('magento.product.attribute') + print binder + print record['attribute_id'] + attribute_binding = binder.to_internal(record['attribute_id']) + + if not attribute_binding: + raise MappingError("The product attribute with " + "magento id %s is not imported." % + record['attribute_id']) + + parent = attribute_binding.odoo_id + return { + 'attribute_id': parent.id, + 'magento_attribute_id': attribute_binding.id, + } diff --git a/connector_magento_configurable/security/ir.model.access.csv b/connector_magento_configurable/security/ir.model.access.csv new file mode 100644 index 000000000..06d0dbda6 --- /dev/null +++ b/connector_magento_configurable/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"access_magento_product_attribute","magento_product_attribute connector manager","model_magento_product_attribute","connector.group_connector_manager",1,1,1,1 +"access_magento_product_attribute_user","magento_product_attribute user","model_magento_product_attribute","sales_team.group_sale_salesman",1,0,0,0 +"access_magento_product_attribute_sale_manager","magento_product_attribute sale manager","model_magento_product_attribute","sales_team.group_sale_manager",1,1,1,1 diff --git a/connector_magento_configurable/views/magento_backend_views.xml b/connector_magento_configurable/views/magento_backend_views.xml new file mode 100644 index 000000000..741c92b81 --- /dev/null +++ b/connector_magento_configurable/views/magento_backend_views.xml @@ -0,0 +1,24 @@ + + + + + magento.backend + + + + +
+
+