From b718c0f1601ed4a03249e59f546f6ef3acc13882 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 20 Mar 2020 12:44:25 -0700 Subject: [PATCH 1/8] IMP `connector_opencart` Support orders with 'coupons' key to build up a discount product line. --- connector_opencart/components/api/opencart.py | 17 +++++++++-- connector_opencart/models/opencart/backend.py | 2 ++ connector_opencart/models/opencart/store.py | 2 ++ .../models/sale_order/importer.py | 29 +++++++++++++++++++ .../views/opencart_backend_views.xml | 2 ++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/connector_opencart/components/api/opencart.py b/connector_opencart/components/api/opencart.py index d1914f74..f0dfe367 100644 --- a/connector_opencart/components/api/opencart.py +++ b/connector_opencart/components/api/opencart.py @@ -5,6 +5,9 @@ import requests from urllib.parse import urlencode from json import loads, dumps +import logging +_logger = logging.getLogger(__name__) + class Opencart: @@ -37,11 +40,19 @@ class Opencart: if params: encoded_url += '?%s' % urlencode(params) headers = self.get_headers(encoded_url, method) - + _logger.debug('send_request method: %s url: %s headers: %s params: %s body: %s' % ( + method, + url, + headers, + params, + body + )) if method == 'GET': - return loads(self.session.get(url, params=params, headers=headers).text) + result_text = self.session.get(url, params=params, headers=headers).text elif method == 'PUT' or method == 'POST': - return loads(self.session.put(url, data=body, headers=headers).text) + result_text = self.session.put(url, data=body, headers=headers).text + _logger.debug('raw_text: ' + str(result_text)) + return loads(result_text) class Resource: diff --git a/connector_opencart/models/opencart/backend.py b/connector_opencart/models/opencart/backend.py index cc362ac0..46620a87 100644 --- a/connector_opencart/models/opencart/backend.py +++ b/connector_opencart/models/opencart/backend.py @@ -62,6 +62,8 @@ class OpencartBackend(models.Model): "in Odoo.", ) # payment_mode_id = fields.Many2one(comodel_name='account.payment.mode', string="Payment Mode") + coupon_product_id = fields.Many2one(comodel_name='product.product', string='Coupon Product', + help='Product to represent coupon discounts.') # New Product fields. product_categ_id = fields.Many2one(comodel_name='product.category', string='Product Category', diff --git a/connector_opencart/models/opencart/store.py b/connector_opencart/models/opencart/store.py index 31e71e39..4444b16f 100644 --- a/connector_opencart/models/opencart/store.py +++ b/connector_opencart/models/opencart/store.py @@ -50,6 +50,8 @@ class OpencartStore(models.Model): "order 36071 in Opencart, will be named 'OC-36071' " "in Odoo. (overridden from backend)", ) + coupon_product_id = fields.Many2one(comodel_name='product.product', string='Coupon Product', + help='Product to represent coupon discounts.') class OpencartStoreAdapter(Component): diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index f97691d9..dd8f1308 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -61,6 +61,34 @@ class SaleOrderImportMapper(Component): children = [('products', 'opencart_order_line_ids', 'opencart.sale.order.line'), ] + def _add_coupon_lines(self, map_record, values): + # Data from API + # 'coupons': [{'amount': '7.68', 'code': '1111'}], + record = map_record.source + + coupons = record.get('coupons') + if not coupons: + return values + + coupon_product = self.options.store.coupon_product_id or self.backend_record.coupon_product_id + if not coupon_product: + coupon_product = self.env.ref('connector_ecommerce.product_product_discount', raise_if_not_found=False) + + if not coupon_product: + raise ValueError('Coupon %s on order requires coupon product in configuration.' % (coupons, )) + for coupon in coupons: + line_builder = self.component(usage='order.line.builder') + line_builder.price_unit = -float(coupon.get('amount', 0.0)) + line_builder.product = coupon_product + # `order.line.builder` does not allow naming. + values = line_builder.get_line() + code = coupon.get('code') + if code: + values['name'] = '%s Code: %s' % (coupon_product.name, code) + line = (0, 0, values) + values['order_line'].append(line) + return values + def _add_shipping_line(self, map_record, values): record = map_record.source @@ -77,6 +105,7 @@ class SaleOrderImportMapper(Component): def finalize(self, map_record, values): values.setdefault('order_line', []) + self._add_coupon_lines(map_record, values) self._add_shipping_line(map_record, values) values.update({ 'partner_id': self.options.partner_id, diff --git a/connector_opencart/views/opencart_backend_views.xml b/connector_opencart/views/opencart_backend_views.xml index 360b9315..716dcc63 100644 --- a/connector_opencart/views/opencart_backend_views.xml +++ b/connector_opencart/views/opencart_backend_views.xml @@ -37,6 +37,7 @@ + @@ -131,6 +132,7 @@ + From 3aa31f8bcbf87c83458ac65189edbe6bd63c4290 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 24 Mar 2020 09:16:30 -0700 Subject: [PATCH 2/8] FIX `connector_opencart` refactor the coupon naming that overwrote the local variable named values --- connector_opencart/models/sale_order/importer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index dd8f1308..ddd20665 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -81,12 +81,11 @@ class SaleOrderImportMapper(Component): line_builder.price_unit = -float(coupon.get('amount', 0.0)) line_builder.product = coupon_product # `order.line.builder` does not allow naming. - values = line_builder.get_line() + line_values = line_builder.get_line() code = coupon.get('code') if code: - values['name'] = '%s Code: %s' % (coupon_product.name, code) - line = (0, 0, values) - values['order_line'].append(line) + line_values['name'] = '%s Code: %s' % (coupon_product.name, code) + values['order_line'].append((0, 0, line_values)) return values def _add_shipping_line(self, map_record, values): From 5cc665df4cb6400a40309e01fa89aa12ac2b8294 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 28 Apr 2020 11:33:24 -0700 Subject: [PATCH 3/8] [IMP] connector_opencart: when finding partners, search for inactive as well. --- .../models/sale_order/importer.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index ddd20665..819e532a 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -9,9 +9,6 @@ from odoo.addons.component.core import Component from odoo.addons.connector.components.mapper import mapping from odoo.exceptions import ValidationError -import logging -_logger = logging.getLogger(__name__) - class SaleOrderBatchImporter(Component): _name = 'opencart.sale.order.batch.importer' @@ -215,21 +212,17 @@ class SaleOrderImporter(Component): return self.env['res.partner'].create(values) def _partner_matches(self, partner, values): - _logger.warn('_partner_matches partner: ' + str(partner) + ' values: ' + str(values)) for key, value in values.items(): if key in ('active', 'parent_id'): continue if key == 'state_id': if value != partner.state_id.id: - _logger.warn(' return false for state_id value: ' + str(value) + ' partner.state_id.id: ' + str(partner.state_id.id)) return False elif key == 'country_id': if value != partner.country_id.id: - _logger.warn(' return false for country_id value: ' + str(value) + ' partner.country_id.id: ' + str(partner.country_id.id)) return False elif bool(value) and value != getattr(partner, key): - _logger.warn(' return false for ' + str(key) + ' : ' + str(value) + ' partner value: ' + str(getattr(partner, key))) return False return True @@ -282,17 +275,15 @@ class SaleOrderImporter(Component): } def _import_addresses(self): - record = self.opencart_record - partner_values = self._get_partner_values() partners = self.env['res.partner'].search([ ('email', '=', partner_values['email']), - ]) + '|', ('active', '=', False), ('active', '=', True), + ], order='active DESC, id ASC') partner = None for possible in partners: if self._partner_matches(possible, partner_values): - _logger.warn('matched partner: ' + str(possible)) partner = possible break if not partner and partners: @@ -307,7 +298,6 @@ class SaleOrderImporter(Component): partner_values['active'] = False shipping_partner = self._create_partner(copy(partner_values)) else: - _logger.warn('same shipping partner') shipping_partner = partner invoice_values = self._get_partner_values(info_string='payment_') @@ -317,7 +307,6 @@ class SaleOrderImporter(Component): # Try to find existing invoice address.... for possible in partners: if self._partner_matches(possible, invoice_values): - _logger.warn('matched invoice partner: ' + str(possible)) invoice_partner = possible break else: @@ -325,10 +314,8 @@ class SaleOrderImporter(Component): partner_values['active'] = False invoice_partner = self._create_partner(copy(invoice_values)) elif self._partner_matches(partner, invoice_values): - _logger.warn('same invoice partner') invoice_partner = partner elif self._partner_matches(shipping_partner, invoice_values): - _logger.warn('same invoice shipping partner') invoice_partner = shipping_partner self.partner = partner From b68d1714a40389fff381968b2a4f1afb077a0a97 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 29 Apr 2020 14:40:20 -0700 Subject: [PATCH 4/8] [IMP] connector_opencart: try to find by model then sku --- connector_opencart/models/product/importer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/connector_opencart/models/product/importer.py b/connector_opencart/models/product/importer.py index d0e68c45..cdfbfcf5 100644 --- a/connector_opencart/models/product/importer.py +++ b/connector_opencart/models/product/importer.py @@ -25,7 +25,8 @@ class ProductImportMapper(Component): @mapping def opencart_sku(self, record): - return {'opencart_sku': (record.get('sku') or '').strip()} + sku = str(record.get('model') or record.get('sku') or '').strip() + return {'opencart_sku': sku} @only_create @mapping @@ -33,15 +34,18 @@ class ProductImportMapper(Component): product_template = self.env['product.template'] template = product_template.browse() - if record.get('sku'): - sku = (record.get('sku') or '').strip() + if record.get('model'): + model = str(record.get('model') or '').strip() # Try to match our own field - template = product_template.search([('opencart_sku', '=', sku)], limit=1) + template = product_template.search([('opencart_sku', '=', model)], limit=1) if not template: # Try to match the default_code - 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) + template = product_template.search([('default_code', '=', model)], limit=1) + if not template and record.get('sku'): + sku = str(record.get('sku') or '').strip() + template = product_template.search([('opencart_sku', '=', sku)], limit=1) + if not template: + template = product_template.search([('default_code', '=', sku)], limit=1) if not template and record.get('name'): name = record.get('product_description', [{}])[0].get('name') if name: From 93178894d475199a773c636326a0d07302044bbc Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 4 May 2020 14:06:09 -0700 Subject: [PATCH 5/8] [FIX] connector_opencart: Compare string to string instead of integer. --- connector_opencart/models/product/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_opencart/models/product/importer.py b/connector_opencart/models/product/importer.py index cdfbfcf5..c7988323 100644 --- a/connector_opencart/models/product/importer.py +++ b/connector_opencart/models/product/importer.py @@ -73,7 +73,7 @@ class ProductImporter(Component): 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']) + option_value = existing_option_values.filtered(lambda v: v.external_id == str(record_option_value['product_option_value_id'])) name = unescape(record_option_value.get('name', '')) if not option_value: option_value = existing_option_values.create({ From 544930f592c6d946caaa15e21935625119bcce5c Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 7 May 2020 08:50:58 -0700 Subject: [PATCH 6/8] [IMP] connector_opencart: Raise warning when product cannot be created/found by attributes, enable logging in upstream. --- connector_opencart/models/product/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/connector_opencart/models/product/common.py b/connector_opencart/models/product/common.py index 8e0a9c0c..4d169803 100644 --- a/connector_opencart/models/product/common.py +++ b/connector_opencart/models/product/common.py @@ -37,7 +37,10 @@ class OpencartProductTemplate(models.Model): raise RetryableJobError('Order Product (%s) has option (%s) "%s" that is not mapped to an Odoo Attribute Value.' % (self, 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) + product = self.odoo_id._create_product_variant(selected_attribute_values, log_warning=True) + if not product: + raise Exception('No product can be created for selected attribute values, check logs. ' + str(selected_attribute_values)) + return product class ProductTemplate(models.Model): From ac29c1ed077f9c6d1aa0b8a28e109a839ce895af Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 19 Jun 2020 11:08:08 -0700 Subject: [PATCH 7/8] [FIX] connector_opencart: Delivery and Invoice address types set before creation. --- connector_opencart/models/sale_order/importer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index 819e532a..b33f4f5f 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -213,7 +213,7 @@ class SaleOrderImporter(Component): def _partner_matches(self, partner, values): for key, value in values.items(): - if key in ('active', 'parent_id'): + if key in ('active', 'parent_id', 'type'): continue if key == 'state_id': @@ -227,7 +227,7 @@ class SaleOrderImporter(Component): return True def _make_partner_name(self, firstname, lastname): - name = (str(firstname) + ' ' + str(lastname)).strip() + name = (str(firstname or '').strip() + ' ' + str(lastname or '').strip()).strip() if not name: return 'Undefined' return name @@ -295,12 +295,14 @@ class SaleOrderImporter(Component): if not self._partner_matches(partner, partner_values): partner_values['parent_id'] = partner.id - partner_values['active'] = False - shipping_partner = self._create_partner(copy(partner_values)) + shipping_values = copy(partner_values) + shipping_values['type'] = 'delivery' + shipping_partner = self._create_partner(shipping_values) else: shipping_partner = partner invoice_values = self._get_partner_values(info_string='payment_') + invoice_values['type'] = 'invoice' if (not self._partner_matches(partner, invoice_values) and not self._partner_matches(shipping_partner, invoice_values)): @@ -310,8 +312,7 @@ class SaleOrderImporter(Component): invoice_partner = possible break else: - partner_values['parent_id'] = partner.id - partner_values['active'] = False + invoice_values['parent_id'] = partner.id invoice_partner = self._create_partner(copy(invoice_values)) elif self._partner_matches(partner, invoice_values): invoice_partner = partner From 3d2de4e80a8c917cbfda3abdbca2fb231cbbe11c Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 14 Jul 2020 14:01:23 -0700 Subject: [PATCH 8/8] [IMP] connector_opencart: Add option on backend to require all SO products to not have checkpoints before importing order. Additionally, allow users to re-bind product templates (understandably risky). --- connector_opencart/components/importer.py | 10 +++++++++- connector_opencart/models/opencart/backend.py | 17 +++++++++++++++++ .../models/sale_order/importer.py | 10 +++++++++- .../views/opencart_backend_views.xml | 1 + .../views/opencart_product_views.xml | 2 +- 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/connector_opencart/components/importer.py b/connector_opencart/components/importer.py index a2c7f973..6a1ef5db 100644 --- a/connector_opencart/components/importer.py +++ b/connector_opencart/components/importer.py @@ -88,7 +88,8 @@ class OpencartImporter(AbstractComponent): if not external_id: return binder = self.binder_for(binding_model) - if always or not binder.to_internal(external_id): + record = binder.to_internal(external_id) + if always or not record: if importer is None: importer = self.component(usage='record.importer', model_name=binding_model) @@ -99,6 +100,13 @@ class OpencartImporter(AbstractComponent): 'Dependency import of %s(%s) has been ignored.', binding_model._name, external_id ) + return True + if binding_model == 'opencart.product.template' and record.backend_id.so_require_product_setup: + # Though this is not the "right" place to do this, + # we need to return True if there is a checkpoint for a product. + if record.backend_id.find_checkpoint(record): + return True + return False def _import_dependencies(self): """ Import the dependencies for the record diff --git a/connector_opencart/models/opencart/backend.py b/connector_opencart/models/opencart/backend.py index 46620a87..42cd62ad 100644 --- a/connector_opencart/models/opencart/backend.py +++ b/connector_opencart/models/opencart/backend.py @@ -73,6 +73,9 @@ class OpencartBackend(models.Model): string='Import sale orders after id', ) + so_require_product_setup = fields.Boolean(string='SO Require Product Setup', + help='Prevents SO from being confirmed (failed queue job), if one or more products has an open checkpoint.') + @contextmanager @api.multi def work_on(self, model_name, **kwargs): @@ -89,6 +92,20 @@ class OpencartBackend(models.Model): return add_checkpoint(self.env, record._name, record.id, self._name, self.id) + @api.multi + def find_checkpoint(self, record): + self.ensure_one() + record.ensure_one() + checkpoint_model = self.env['connector.checkpoint'] + model_model = self.env['ir.model'] + model = model_model.search([('model', '=', record._name)], limit=1) + return checkpoint_model.search([ + ('backend_id', '=', '%s,%s' % (self._name, self.id)), + ('model_id', '=', model.id), + ('record_id', '=', record.id), + ('state', '=', 'need_review'), + ], limit=1) + @api.multi def synchronize_metadata(self): try: diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index b33f4f5f..79e19595 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -8,6 +8,7 @@ from odoo import fields, _ from odoo.addons.component.core import Component from odoo.addons.connector.components.mapper import mapping from odoo.exceptions import ValidationError +from odoo.addons.queue_job.exception import RetryableJobError class SaleOrderBatchImporter(Component): @@ -363,9 +364,16 @@ class SaleOrderImporter(Component): def _import_dependencies(self): record = self.opencart_record self._import_addresses() + products_need_setup = [] for product in record.get('products', []): if 'product_id' in product and product['product_id']: - self._import_dependency(product['product_id'], 'opencart.product.template') + needs_product_setup = self._import_dependency(product['product_id'], 'opencart.product.template') + if needs_product_setup: + products_need_setup.append(product['product_id']) + + if products_need_setup and self.backend_record.so_require_product_setup: + # There are products that were either just imported, or + raise RetryableJobError('Products need setup. OpenCart Product IDs:' + str(products_need_setup), seconds=3600) class SaleOrderLineImportMapper(Component): diff --git a/connector_opencart/views/opencart_backend_views.xml b/connector_opencart/views/opencart_backend_views.xml index 716dcc63..d60fda7b 100644 --- a/connector_opencart/views/opencart_backend_views.xml +++ b/connector_opencart/views/opencart_backend_views.xml @@ -38,6 +38,7 @@ + diff --git a/connector_opencart/views/opencart_product_views.xml b/connector_opencart/views/opencart_product_views.xml index fec97208..4cec551f 100644 --- a/connector_opencart/views/opencart_product_views.xml +++ b/connector_opencart/views/opencart_product_views.xml @@ -9,7 +9,7 @@
- +