From 6c45c0380c9b8b5e4205e0bf4412ba52705a503f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 18 Aug 2021 13:29:09 -0700 Subject: [PATCH] [FIX] connector_opencart: find orders by modified date instead of ID Opencart will not always generate a new order id, and may update the date_added but not actually be ready for import. To mitigate, we're now looking for orders modified after a set datetime. Additionally, offer mechanisms to conver datetime to/from Opencart's timezone. This is used during the above import behavior, as well as the `date_order` field in Odoo. --- connector_opencart/__manifest__.py | 2 +- connector_opencart/components/api/opencart.py | 12 ++++-- connector_opencart/models/opencart/backend.py | 43 +++++++++++++++++-- .../models/sale_order/common.py | 9 +++- .../models/sale_order/importer.py | 11 ++++- .../views/opencart_backend_views.xml | 10 ++--- 6 files changed, 70 insertions(+), 17 deletions(-) diff --git a/connector_opencart/__manifest__.py b/connector_opencart/__manifest__.py index af9c90e5..c31baaec 100644 --- a/connector_opencart/__manifest__.py +++ b/connector_opencart/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Opencart Connector', - 'version': '12.0.1.2.0', + 'version': '12.0.1.3.0', 'category': 'Connector', 'depends': [ 'account', diff --git a/connector_opencart/components/api/opencart.py b/connector_opencart/components/api/opencart.py index f0dfe367..db6487d8 100644 --- a/connector_opencart/components/api/opencart.py +++ b/connector_opencart/components/api/opencart.py @@ -4,6 +4,7 @@ import requests from urllib.parse import urlencode from json import loads, dumps +from json.decoder import JSONDecodeError import logging _logger = logging.getLogger(__name__) @@ -52,7 +53,10 @@ class Opencart: elif method == 'PUT' or method == 'POST': result_text = self.session.put(url, data=body, headers=headers).text _logger.debug('raw_text: ' + str(result_text)) - return loads(result_text) + try: + return loads(result_text) + except JSONDecodeError: + return {} class Resource: @@ -75,10 +79,13 @@ class Orders(Resource): path = 'orders' - def all(self, id_larger_than=None): + def all(self, id_larger_than=None, modified_from=None): url = self.url if id_larger_than: url += '/id_larger_than/%s' % id_larger_than + if modified_from: + # TODO remove details if it gets into main route + url += 'details/modified_from/%s' % modified_from return self.connection.send_request(method='GET', url=url) def get(self, id): @@ -97,7 +104,6 @@ class Orders(Resource): )) return res - def cancel(self, id): url = self.connection.base_url + ('order_status/%s' % id) return self.connection.send_request(method='POST', url=url, body=self.get_status_payload('Canceled')) diff --git a/connector_opencart/models/opencart/backend.py b/connector_opencart/models/opencart/backend.py index 0f0aff12..dd1767d3 100644 --- a/connector_opencart/models/opencart/backend.py +++ b/connector_opencart/models/opencart/backend.py @@ -4,6 +4,7 @@ from logging import getLogger from contextlib import contextmanager +from datetime import timedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -69,8 +70,17 @@ class OpencartBackend(models.Model): product_categ_id = fields.Many2one(comodel_name='product.category', string='Product Category', help='Default product category for newly created products.') + # renamed, and not used when searching orders anymore import_orders_after_id = fields.Integer( - string='Import sale orders after id', + string='Highest Order ID', + ) + # Note that Opencart may not return timestamps in UTC + import_orders_after_date = fields.Datetime( + string='Import Orders Modified After', + ) + server_offset_hours = fields.Float( + string='Opencart Server Timezone Offset', + help='E.g. US Pacific is -8.0, the important thing is to either not change this during DST or to adjust the import_orders_after_date field at the same time.', ) so_require_product_setup = fields.Boolean(string='SO Require Product Setup', @@ -134,14 +144,16 @@ class OpencartBackend(models.Model): backends = self.search([ ('base_url', '!=', False), ('restadmin_token', '!=', False), - ('import_orders_after_id', '!=', False), + ('import_orders_after_date', '!=', False), ('scheduler_order_import', '=', True), ]) return backends.import_sale_orders() @api.multi def import_sale_orders(self): - self._import_after_id('opencart.sale.order', 'import_orders_after_id') + # self._import_after_id('opencart.sale.order', 'import_orders_after_id') + # import_orders_after_date + self._import_sale_orders_after_date() return True @api.multi @@ -152,3 +164,28 @@ class OpencartBackend(models.Model): backend, filters={'after_id': after_id} ) + + @api.multi + def _import_sale_orders_after_date(self): + for backend in self: + date = backend.date_to_opencart(backend.import_orders_after_date) + date = str(date).replace(' ', 'T') + self.env['opencart.sale.order'].with_delay().import_batch( + backend, + filters={'modified_from': date} + ) + + def date_to_opencart(self, date): + # date provided should be UTC and will be converted to Opencart's dates + return self._date_plus_hours(date, self.server_offset_hours or 0) + + def date_to_odoo(self, date): + # date provided should be in Opencart's TZ, converted to UTC + return self._date_plus_hours(date, -(self.server_offset_hours or 0)) + + def _date_plus_hours(self, date, hours): + if not hours: + return date + if isinstance(date, str): + date = fields.Datetime.from_string(date) + return date + timedelta(hours=self.server_offset_hours) diff --git a/connector_opencart/models/sale_order/common.py b/connector_opencart/models/sale_order/common.py index 2ad4f606..3e91b8f6 100644 --- a/connector_opencart/models/sale_order/common.py +++ b/connector_opencart/models/sale_order/common.py @@ -96,7 +96,12 @@ class SaleOrderAdapter(Component): def search(self, filters=None): api_instance = self.api_instance - orders_response = api_instance.orders.all(id_larger_than=filters.get('after_id')) + api_filters = {} + if 'after_id' in filters: + api_filters['id_larger_than'] = filters['after_id'] + if 'modified_from' in filters: + api_filters['modified_from'] = filters['modified_from'] + orders_response = api_instance.orders.all(**api_filters) if 'error' in orders_response and orders_response['error']: raise ValidationError(str(orders_response)) @@ -105,7 +110,7 @@ class SaleOrderAdapter(Component): orders = orders_response['data'] # Note that `store_id is None` is checked as it may not be in the output. - return map(lambda o: (o['order_id'], o.get('store_id', None)), orders) + return map(lambda o: (o['order_id'], o.get('store_id', None), o.get('date_modified') or o.get('date_added')), orders) def read(self, id): api_instance = self.api_instance diff --git a/connector_opencart/models/sale_order/importer.py b/connector_opencart/models/sale_order/importer.py index 7d7945a5..fa37610a 100644 --- a/connector_opencart/models/sale_order/importer.py +++ b/connector_opencart/models/sale_order/importer.py @@ -51,7 +51,11 @@ class SaleOrderBatchImporter(Component): self._import_record(ids[0], ids[1]) if external_ids: last_id = list(sorted(external_ids, key=lambda i: i[0]))[-1][0] - self.backend_record.import_orders_after_id = last_id + last_date = list(sorted(external_ids, key=lambda i: i[2]))[-1][2] + self.backend_record.write({ + 'import_orders_after_id': last_id, + 'import_orders_after_date': self.backend_record.date_to_odoo(last_date), + }) class SaleOrderImportMapper(Component): @@ -133,7 +137,10 @@ class SaleOrderImportMapper(Component): @mapping def date_order(self, record): - return {'date_order': record.get('date_added', fields.Datetime.now())} + date_added = record.get('date_added') + if date_added: + date_added = self.backend_record.date_to_odoo(date_added) + return {'date_order': date_added or fields.Datetime.now()} @mapping def fiscal_position_id(self, record): diff --git a/connector_opencart/views/opencart_backend_views.xml b/connector_opencart/views/opencart_backend_views.xml index 1b9138fe..be7adaad 100644 --- a/connector_opencart/views/opencart_backend_views.xml +++ b/connector_opencart/views/opencart_backend_views.xml @@ -23,6 +23,7 @@ + @@ -70,12 +71,8 @@ in the menu 'Connectors > Checkpoint'.

-
-
+ +