From ce8bb41c0353d3b3774625a48c8a47bf2d5731e8 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 20 Feb 2020 15:03:10 -0800 Subject: [PATCH] IMP `connector_opencart` Add bindings between Opencart Product Option Values and Odoo's Product Template Attribute Values --- connector_opencart/__manifest__.py | 1 + connector_opencart/components/api/opencart.py | 15 ++++ connector_opencart/components/binder.py | 2 + connector_opencart/models/__init__.py | 1 + connector_opencart/models/opencart/backend.py | 9 ++- connector_opencart/models/product/__init__.py | 2 + connector_opencart/models/product/common.py | 77 ++++++++++++++++++ connector_opencart/models/product/importer.py | 78 +++++++++++++++++++ .../models/sale_order/importer.py | 35 +++------ .../security/ir.model.access.csv | 2 + .../views/opencart_product_views.xml | 28 +++++++ 11 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 connector_opencart/models/product/__init__.py create mode 100644 connector_opencart/models/product/common.py create mode 100644 connector_opencart/models/product/importer.py create mode 100644 connector_opencart/views/opencart_product_views.xml diff --git a/connector_opencart/__manifest__.py b/connector_opencart/__manifest__.py index 750f30c6..c6ea2bb9 100644 --- a/connector_opencart/__manifest__.py +++ b/connector_opencart/__manifest__.py @@ -21,6 +21,7 @@ 'security/ir.model.access.csv', 'views/delivery_views.xml', 'views/opencart_backend_views.xml', + 'views/opencart_product_views.xml', ], 'installable': True, 'application': False, diff --git a/connector_opencart/components/api/opencart.py b/connector_opencart/components/api/opencart.py index cd52828f..d1914f74 100644 --- a/connector_opencart/components/api/opencart.py +++ b/connector_opencart/components/api/opencart.py @@ -22,6 +22,10 @@ class Opencart: def stores(self): return Stores(connection=self) + @property + def products(self): + return Products(connection=self) + def get_headers(self, url, method): headers = {} if method in ('POST', 'PUT', ): @@ -138,3 +142,14 @@ class Stores(Resource): def get(self, id): url = self.url + ('/%s' % id) return self.connection.send_request(method='GET', url=url) + + +class Products(Resource): + """ + Retrieves Product details + """ + path = 'products' + + def get(self, id): + url = self.url + ('/%s' % id) + return self.connection.send_request(method='GET', url=url) diff --git a/connector_opencart/components/binder.py b/connector_opencart/components/binder.py index d8bbaebd..b2525fea 100644 --- a/connector_opencart/components/binder.py +++ b/connector_opencart/components/binder.py @@ -20,4 +20,6 @@ class OpencartModelBinder(Component): 'opencart.sale.order', 'opencart.sale.order.line', 'opencart.stock.picking', + 'opencart.product.template', + 'opencart.product.template.attribute.value', ] diff --git a/connector_opencart/models/__init__.py b/connector_opencart/models/__init__.py index 4ed0f156..a7f55577 100644 --- a/connector_opencart/models/__init__.py +++ b/connector_opencart/models/__init__.py @@ -1,4 +1,5 @@ from . import delivery from . import opencart +from . import product from . import sale_order from . import stock_picking diff --git a/connector_opencart/models/opencart/backend.py b/connector_opencart/models/opencart/backend.py index f3b344ce..cc362ac0 100644 --- a/connector_opencart/models/opencart/backend.py +++ b/connector_opencart/models/opencart/backend.py @@ -2,12 +2,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta from logging import getLogger from contextlib import contextmanager from odoo import api, fields, models, _ from odoo.exceptions import UserError +from odoo.addons.connector.models.checkpoint import add_checkpoint from ...components.api.opencart import Opencart _logger = getLogger(__name__) @@ -80,6 +80,13 @@ class OpencartBackend(models.Model): with _super.work_on(model_name, opencart_api=opencart_api, **kwargs) as work: yield work + @api.multi + def add_checkpoint(self, record): + self.ensure_one() + record.ensure_one() + return add_checkpoint(self.env, record._name, record.id, + self._name, self.id) + @api.multi def synchronize_metadata(self): try: diff --git a/connector_opencart/models/product/__init__.py b/connector_opencart/models/product/__init__.py new file mode 100644 index 00000000..79ab5dc6 --- /dev/null +++ b/connector_opencart/models/product/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import importer diff --git a/connector_opencart/models/product/common.py b/connector_opencart/models/product/common.py new file mode 100644 index 00000000..c4afcaa7 --- /dev/null +++ b/connector_opencart/models/product/common.py @@ -0,0 +1,77 @@ +from odoo import api, fields, models +from odoo.addons.queue_job.exception import NothingToDoJob, RetryableJobError +from odoo.addons.component.core import Component + + +class OpencartProductTemplate(models.Model): + _name = 'opencart.product.template' + _inherit = 'opencart.binding' + _inherits = {'product.template': 'odoo_id'} + _description = 'Opencart Product' + + odoo_id = fields.Many2one('product.template', + string='Product', + required=True, + ondelete='restrict') + opencart_attribute_value_ids = fields.One2many('opencart.product.template.attribute.value', + 'opencart_product_tmpl_id', + string='Opencart Product Attribute Values') + + def opencart_sale_get_combination(self, options, reentry=False): + selected_attribute_values = self.env['product.template.attribute.value'] + for option in options: + product_option_value_id = str(option['product_option_value_id']) + opencart_attribute_value = self.opencart_attribute_value_ids.filtered(lambda v: v.external_id == product_option_value_id) + if not opencart_attribute_value: + if reentry: + # we have already triggered an import. + raise Exception('Order Product has option (%s) "%s" that does not exist on the product.' % (product_option_value_id, option.get('name', ''))) + # need to re-import product. + try: + self.import_record(self.backend_id, self.external_id, force=True) + return self.opencart_sale_get_combination(options, reentry=True) + except NothingToDoJob: + if reentry: + raise RetryableJobError('Product imported, but selected option is not available.') + if not opencart_attribute_value.odoo_id: + raise RetryableJobError('Order Product has option (%s) "%s" that is not mapped to an Odoo Attribute Value.' % (opencart_attribute_value.external_id, opencart_attribute_value.opencart_name)) + selected_attribute_values += opencart_attribute_value.odoo_id + # Now that we know what options are selected, we can load a variant with those options + return self.odoo_id._create_product_variant(selected_attribute_values) + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + opencart_bind_ids = fields.One2many('opencart.product.template', 'odoo_id', string='Opencart Bindings') + + +class OpencartProductTemplateAdapter(Component): + _name = 'opencart.product.template.adapter' + _inherit = 'opencart.adapter' + _apply_on = 'opencart.product.template' + + def read(self, id): + api_instance = self.api_instance + record = api_instance.products.get(id) + if 'data' in record and record['data']: + return record['data'] + raise RetryableJobError('Product "' + str(id) + '" did not return an product response. ' + str(record)) + + +# Product Attribute Value, cannot "inherits" the odoo_id as then it cannot be empty +class OpencartProductTemplateAttributeValue(models.Model): + _name = 'opencart.product.template.attribute.value' + _inherit = 'opencart.binding' + _description = 'Opencart Product Attribute Value' + + odoo_id = fields.Many2one('product.template.attribute.value', + string='Product Attribute Value', + required=False, + ondelete='cascade') + opencart_name = fields.Char(string='Opencart Name', help='For matching purposes.') + opencart_product_tmpl_id = fields.Many2one('opencart.product.template', + string='Opencart Product', + required=True, + ondelete='cascade') + product_tmpl_id = fields.Many2one(related='opencart_product_tmpl_id.odoo_id') diff --git a/connector_opencart/models/product/importer.py b/connector_opencart/models/product/importer.py new file mode 100644 index 00000000..b45a6ce0 --- /dev/null +++ b/connector_opencart/models/product/importer.py @@ -0,0 +1,78 @@ +from html import unescape +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create + + +class ProductImportMapper(Component): + _name = 'opencart.product.template.import.mapper' + _inherit = 'opencart.import.mapper' + _apply_on = ['opencart.product.template'] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + @mapping + def name(self, record): + name = record.get('product_description', [{}])[0].get('name', record.get('id')) + return {'name': unescape(name)} + + # TODO more fields like pricing.... + + @mapping + def product_type(self, record): + return {'type': 'product' if record.get('shipping') else 'service'} + + @only_create + @mapping + def existing_product(self, record): + product_template = self.env['product.template'] + template = product_template.browse() + + if record.get('sku'): + template = product_template.search([('default_code', '=', record.get('sku'))], limit=1) + if not template and record.get('model'): + template = product_template.search([('default_code', '=', record.get('model'))], limit=1) + if not template and record.get('name'): + name = record.get('product_description', [{}])[0].get('name') + if name: + template = product_template.search([('name', '=', unescape(name))], limit=1) + return {'odoo_id': template.id} + + +class ProductImporter(Component): + _name = 'opencart.product.template.importer' + _inherit = 'opencart.importer' + _apply_on = ['opencart.product.template'] + + def _create(self, data): + binding = super(ProductImporter, self)._create(data) + self.backend_record.add_checkpoint(binding) + return binding + + def _after_import(self, binding): + self._sync_options(binding) + + def _sync_options(self, binding): + existing_option_values = binding.opencart_attribute_value_ids + mapped_option_values = binding.opencart_attribute_value_ids.browse() + record = self.opencart_record + backend = self.backend_record + for option in record.get('options', []): + for record_option_value in option.get('option_value', []): + option_value = existing_option_values.filtered(lambda v: v.external_id == record_option_value['product_option_value_id']) + name = unescape(record_option_value.get('name', '')) + if not option_value: + option_value = existing_option_values.create({ + 'backend_id': backend.id, + 'external_id': record_option_value['product_option_value_id'], + 'opencart_name': name, + 'opencart_product_tmpl_id': binding.id, + }) + # Keep options consistent with Opencart by renaming them + if option_value.opencart_name != name: + option_value.opencart_name = name + mapped_option_values += option_value + + to_unlink = existing_option_values - mapped_option_values + to_unlink.unlink() diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index 123ba001..eb47874d 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -316,7 +316,11 @@ class SaleOrderImporter(Component): return binding def _import_dependencies(self): + record = self.opencart_record self._import_addresses() + for product in record.get('products', []): + if 'product_id' in product and product['product_id']: + self._import_dependency(product['product_id'], 'opencart.product.template') class SaleOrderLineImportMapper(Component): @@ -330,33 +334,18 @@ class SaleOrderLineImportMapper(Component): ('order_product_id', 'external_id'), ] - def _finalize_product_values(self, record, values): - # This would be a good place to create a vendor or add a route... - return values - - def _product_values(self, record): - reference = record['model'] - values = { - 'default_code': reference, - 'name': unescape(record.get('name', reference)), # unknown if other fields, but have observed " in product names - 'type': 'product', - 'list_price': record.get('price', 0.0), - 'categ_id': self.backend_record.product_categ_id.id, - } - return self._finalize_product_values(record, values) - @mapping def name(self, record): return {'name': unescape(record['name'])} @mapping def product_id(self, record): - reference = record['model'] - product = self.env['product.product'].search([ - ('default_code', '=', reference) - ], limit=1) - - if not product: - # we could use a record like (0, 0, values) - product = self.env['product.product'].create(self._product_values(record)) + product_id = record['product_id'] + binder = self.binder_for('opencart.product.template') + # do not unwrap, because it would be a product.template, but I need a specific variant + opencart_product_template = binder.to_internal(product_id, unwrap=False) + if record.get('option'): + product = opencart_product_template.opencart_sale_get_combination(record.get('option')) + else: + product = opencart_product_template.odoo_id.product_variant_id return {'product_id': product.id, 'product_uom': product.uom_id.id} diff --git a/connector_opencart/security/ir.model.access.csv b/connector_opencart/security/ir.model.access.csv index 44dcff29..2a8d2dfd 100644 --- a/connector_opencart/security/ir.model.access.csv +++ b/connector_opencart/security/ir.model.access.csv @@ -4,6 +4,8 @@ "access_opencart_binding","opencart_binding connector manager","model_opencart_binding","connector.group_connector_manager",1,1,1,1 "access_opencart_sale_order","opencart_sale_order connector manager","model_opencart_sale_order","connector.group_connector_manager",1,1,1,1 "access_opencart_sale_order_line","opencart_sale_order_line connector manager","model_opencart_sale_order_line","connector.group_connector_manager",1,1,1,1 +"access_opencart_product_template","opencart_product_template connector manager","model_opencart_product_template","connector.group_connector_manager",1,1,1,1 +"access_opencart_product_template_attribute_value","opencart_product_template_attribute_value connector manager","model_opencart_product_template_attribute_value","connector.group_connector_manager",1,1,1,1 "access_opencart_stock_picking","opencart_stock_picking connector manager","model_opencart_stock_picking","connector.group_connector_manager",1,1,1,1 "access_opencart_sale_order_sale_salesman","opencart_sale_order","model_opencart_sale_order","sales_team.group_sale_salesman",1,0,0,0 "access_opencart_sale_order_sale_manager","opencart_sale_order","model_opencart_sale_order","sales_team.group_sale_manager",1,1,1,1 diff --git a/connector_opencart/views/opencart_product_views.xml b/connector_opencart/views/opencart_product_views.xml new file mode 100644 index 00000000..db349957 --- /dev/null +++ b/connector_opencart/views/opencart_product_views.xml @@ -0,0 +1,28 @@ + + + + + opencart.product.template.form + opencart.product.template + +
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file