def write(self, model, id, data): """ Update records on the external system """ try: start = datetime.now() try: result = self.server.write(self.uid, self.password, model, id, data) except Exception as err: _logger.error("write(%s, %s, %s) failed", model, id, data) raise RetryableJobError(err) else: _logger.debug("write(%s, %s, %s) returned %s in %s seconds", model, id, data, result, (datetime.now() - start).seconds) return result except (socket.gaierror, socket.error, socket.timeout) as err: raise NetworkRetryableError( 'A network error caused the failure of the job: ' '%s' % err) except xmlrpc.client.ProtocolError as err: if err.errcode in [ 502, # Bad gateway 503, # Service unavailable 504 ]: # Gateway timeout raise RetryableJobError( 'A protocol error caused the failure of the job:\n' 'URL: %s\n' 'HTTP/HTTPS headers: %s\n' 'Error code: %d\n' 'Error message: %s\n' % (err.url, err.headers, err.errcode, err.errmsg)) else: raise
def _before_import(self): if self.env['amazon.config.order.status'].browse(self.amazon_record.get('order_status_id')).name not in ('Canceled', 'Pending'): if self.amazon_record and not self.amazon_record.get('lines'): try: self.backend_adapter.get_lines(filters=[self.amazon_record]) if not self.amazon_record.get('lines'): status = self.backend_record.env['amazon.config.order.status'].browse(self.amazon_record.get('order_status_id')).name if status and status not in ('Canceled', 'Pending'): raise FailedJobError("Error recovering lines of the record (%s)", self.amazon_record['order_id']) raise RetryableJobError('The record haven\'t got the lines of items', 60, True) except Exception as e: raise RetryableJobError('The record haven\'t got the lines of items', 60, True) return
def advisory_lock_or_retry(self, lock, retry_seconds=1): """ Acquire a Postgres transactional advisory lock or retry job When the lock cannot be acquired, it raises a :exc:`odoo.addons.queue_job.exception.RetryableJobError` so the job is retried after n ``retry_seconds``. Usage example: .. code-block:: python lock_name = 'import_record({}, {}, {}, {})'.format( self.backend_record._name, self.backend_record.id, self.model._name, self.external_id, ) self.advisory_lock_or_retry(lock_name, retry_seconds=2) See :func:`odoo.addons.connector.connector.pg_try_advisory_lock` for details. :param lock: The lock name. Can be anything convertible to a string. It needs to represent what should not be synchronized concurrently, usually the string will contain at least: the action, the backend name, the backend id, the model name, the external id :param retry_seconds: number of seconds after which a job should be retried when the lock cannot be acquired. """ if not pg_try_advisory_lock(self.env, lock): raise RetryableJobError('Could not acquire advisory lock', seconds=retry_seconds, ignore_retry=True)
def _retry_unique_violation(self): """ Context manager: catch Unique constraint error and retry the job later. When we execute several jobs workers concurrently, it happens that 2 jobs are creating the same record at the same time (especially product templates as they are shared by a lot of sales orders), resulting in: IntegrityError: duplicate key value violates unique constraint "jira_project_project_external_id_uniq" DETAIL: Key (backend_id, external_id)=(1, 4851) already exists. In that case, we'll retry the import just later. """ try: yield except IntegrityError as err: if err.pgcode == errorcodes.UNIQUE_VIOLATION: raise RetryableJobError( 'A database error caused the failure of the job:\n' '%s\n\n' 'Likely due to 2 concurrent jobs wanting to create ' 'the same record. The job will be retried later.' % err) else: raise
def moves_auto_assign(self, locations): """Job trying to reserve moves based on product and locations When a product has been added to a location, it searches all* the moves with a source equal or above this location and try to reserve them. * all the moves that would make sense to reserve, so no chained moves, no MTO, ... """ self.ensure_one() moves = self.env["stock.move"].search(self._moves_auto_assign_domain(locations)) pickings = moves.picking_id if not pickings: return try: self.env.cr.execute( "SELECT id FROM stock_picking WHERE id IN %s FOR UPDATE NOWAIT", (tuple(pickings.ids),), ) except psycopg2.OperationalError as err: if err.pgcode == "55P03": # could not obtain the lock _logger.debug( "Another job is already auto-assigning moves and acquired a" " lock on one or some of stock.picking%s, retry later.", tuple(pickings.ids), ) raise RetryableJobError( "Could not obtain lock on transfers, will retry.", ignore_retry=True ) raise moves._action_assign()
def build_post_init(self, build, post_init_action=None, key_value_dict=None): if post_init_action is None: post_init_action = "" if key_value_dict is None: key_value_dict = {} key_value_dict.update(self._get_mandatory_args(build)) code = self._get_mandatory_code() + post_init_action action = { 'name': 'Build Code Eval', 'state': 'code', 'model_id': 1, 'code': string.Formatter().vformat(code, (), SafeDict(**key_value_dict)) } try: action_ids = self.build_execute_kw(build, 'ir.actions.server', 'create', [action]) self.build_execute_kw(build, 'ir.actions.server', 'run', [action_ids]) except psycopg2.OperationalError as e: # TODO: this is very stupid hack that can be handled only in local instance operators raise RetryableJobError(str(e))
def _retry_unique_violation(self): """ Context manager: catch Unique constraint error and retry the job later. When we execute several jobs workers concurrently, it happens that 2 jobs are creating the same record at the same time (binding record created by :meth:`_export_dependency`), resulting in: IntegrityError: duplicate key value violates unique constraint "jira_project_project_odoo_uniq" DETAIL: Key (backend_id, odoo_id)=(1, 4851) already exists. In that case, we'll retry the import just later. """ try: yield except psycopg2.IntegrityError as err: if err.pgcode == psycopg2.errorcodes.UNIQUE_VIOLATION: raise RetryableJobError( 'A database error caused the failure of the job:\n' '%s\n\n' 'Likely due to 2 concurrent jobs wanting to create ' 'the same record. The job will be retried later.' % err) else: raise
def _handle_lock_failed(self, timestamp): _logger.warning("Failed to acquire timestamps %s", timestamp, exc_info=True) raise RetryableJobError( 'Concurrent job / process already syncing', ignore_retry=True, )
def import_record(self, backend, external_id): _super = super(AmazonProductProduct, self) try: result = _super.import_record(backend, external_id) if not result: raise RetryableJobError( 'The product of the backend %s hasn\'t could not be imported. \n %s', backend.name, external_id, 60) except Exception as e: if e.message.find( 'current transaction is aborted') > -1 or e.message.find( 'could not serialize access due to concurrent update' ) > -1: raise RetryableJobError( 'A concurrent job is already exporting the same record ' '(%s). The job will be retried later.' % self.model._name, 60, True) raise e
def testing_method(self, *args, **kwargs): """ Method used for tests Return always the arguments and keyword arguments received """ if kwargs.get("raise_retry"): raise RetryableJobError("Must be retried later") if kwargs.get("return_context"): return self.env.context return args, kwargs
def call(self, method=None, resource=None, arguments=None): try: location = self._location._location cons_key = self._location.consumer_key sec_key = self._location.consumer_secret version = self._location.version or 'v3' # WooCommerce API Connection wcapi = API( url=location, # Your store URL consumer_key=cons_key, # Your consumer key consumer_secret=sec_key, # Your consumer secret version=version, # WooCommerce WP REST API version query_string_auth=True # Force Basic Authentication as query # string true and using under HTTPS ) if wcapi: if isinstance(arguments, list): while arguments and arguments[-1] is None: arguments.pop() start = datetime.now() try: wooapi = getattr(wcapi, method) res = wooapi(resource) if method not in ['put', 'post'] \ else wooapi(resource, arguments) vals = res.json() if not res.ok: raise FailedJobError(vals) result = vals except: _logger.error("api.call(%s, %s, %s) failed", method, resource, arguments) raise else: _logger.debug("api.call(%s, %s, %s) returned %s in %s\ seconds", method, resource, arguments, result, (datetime.now() - start).seconds) return result except (socket.gaierror, socket.error, socket.timeout) as err: raise NetworkRetryableError( 'A network error caused the failure of the job: ' '%s' % err) except xmlrpc.client.ProtocolError as err: if err.errcode in [502, # Bad gateway 503, # Service unavailable 504]: # Gateway timeout raise RetryableJobError( 'A protocol error caused the failure of the job:\n' 'URL: %s\n' 'HTTP/HTTPS headers: %s\n' 'Error code: %d\n' 'Error message: %s\n' % (err.url, err.headers, err.errcode, err.errmsg)) else: raise
def _before_import(self): if self.amazon_record and self.amazon_record.get('lines'): importer = self.component(usage='amazon.product.category', model_name='amazon.product.product') record = {'marketplace_id':self.amazon_record['marketplace'].id, 'product_product_market_ids':[ {'marketplace_id':self.amazon_record['marketplace'].id, 'sku':line['sku']} for line in self.amazon_record['lines']]} importer.run(record) elif self.amazon_record and not self.amazon_record.get('lines'): try: self.backend_adapter.get_lines(filters=[self.amazon_record]) if not self.amazon_record.get('lines'): status = self.backend_record.env['amazon.config.order.status'].browse(self.amazon_record.get('order_status_id')).name if status and status != 'Cancelled': raise FailedJobError("Error recovering lines of the record (%s)", self.amazon_record['order_id']) raise RetryableJobError('The record haven\'t got the lines of items', 60, True) except: raise RetryableJobError('The record haven\'t got the lines of items', 60, True) return
def throw_retry_exception_for_throttled(self, request_name, backend_id, ignore_retry=True): control = self.search([('request_name', '=', request_name)]) time_now = datetime.now() i = 0 while i < control.max_quota: self.env['amazon.control.date.request'].create({'backend_id':backend_id, 'request_id':control.id, 'request_date':time_now.isoformat()}) i += 1 raise RetryableJobError("MWS API has been an error produced on %s for Request Throttled" % request_name, seconds=ceil(control.restore_rate) + 1, ignore_retry=ignore_retry)
def use_quota_if_avaiable(self, request_name, backend_id): # TODO: finish the method to control the quota of the methods assert request_name control = self.search([('request_name', '=', request_name)]) try: assert control except AssertionError: raise FailedJobError("The action %s doesn't exist on the quota control module (MWS)" % request_name) self.env['amazon.control.date.request'].update({'backend_id':str(backend_id)}) last_requests = self.env['amazon.control.date.request'].search([('backend_id', '=', backend_id), ('request_id', '=', control.id)], order='request_date desc', limit=control.max_quota - ceil(control.max_quota / 3)) time_now = datetime.now() if last_requests and len(last_requests) >= control.max_quota - ceil(control.max_quota / 3): first_date_request = datetime.strptime(last_requests[len(last_requests) - 1].request_date, '%Y-%m-%d %H:%M:%S') if (time_now - first_date_request).seconds < control.restore_rate: raise RetryableJobError("The quota of %s is empty (MWS)" % request_name, seconds=ceil(control.restore_rate * 1.2), ignore_retry=True) if control.max_request_quota_time > 0: last_requests = self.env['amazon.control.date.request'].search([('backend_id', '=', backend_id), ('request_id', '=', control.id)], order='request_date desc', limit=control.max_request_quota_time) max_request_factor = 1 if control.units_of_mesaure_quota == 'min': max_request_factor = 60 if control.units_of_mesaure_quota == 'hor': max_request_factor = 60 * 60 if control.units_of_mesaure_quota == 'day': max_request_factor = 60 * 60 * 24 if last_requests and len(last_requests) >= control.max_request_quota_time: first_date_request = datetime.strptime(last_requests[len(last_requests) - 1].request_date, '%Y-%m-%d %H:%M:%S') if (time_now - first_date_request).seconds < (control.max_request_quota_units * max_request_factor): raise RetryableJobError("The quota of %s is empty (MWS)" % request_name, seconds=ceil(control.restore_rate * 1.2), ignore_retry=True) self.env['amazon.control.date.request'].create({'backend_id':backend_id, 'request_id':control.id, 'request_date':time_now.isoformat()})
def import_record(self, backend, external_id): _super = super(AmazonSaleOrder, self) result = _super.import_record(backend, external_id) if not result: raise RetryableJobError( msg= "The sale of the backend %s hasn\'t could not be imported.\n %s" % (backend.name, external_id), seconds=random.randint(90, 600)) if isinstance(result, Exception): raise Exception(result)
def _after_import(self, binding): """ Hook called at the end of the import Recalculate the prices with taxes (all the sales on Amazon have the prices included) """ try: backend = self.backend_record if binding.odoo_id.order_line: # Compute the prices of lines without taxes amount_untaxed = amount_tax = 0.0 for line in binding.odoo_id.order_line: # Get the default sale tax of the company if not line.tax_id: line.tax_id = backend.env['account.tax'].browse(backend.env['ir.values'].get_default('product.template', 'taxes_id', company_id=backend.company_id.id)) if not line.tax_id.price_include and line.tax_id: taxes = line.tax_id._compute_amount_taxes(line.price_unit, line.product_uom_qty) line.update({ 'price_tax':taxes, 'price_total':line.price_unit, 'price_subtotal':line.price_unit - taxes, }) amount_untaxed += line.price_subtotal # FORWARDPORT UP TO 10.0 if binding.odoo_id.company_id.tax_calculation_rounding_method == 'round_globally': price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.tax_id.compute_all_with_taxes(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=binding.odoo_id.partner_shipping_id) amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])) else: amount_tax += line.price_tax binding.odoo_id.update({ 'amount_untaxed':binding.odoo_id.pricelist_id.currency_id.round(amount_untaxed), 'amount_tax':binding.odoo_id.pricelist_id.currency_id.round(amount_tax), 'amount_total':amount_untaxed + amount_tax, }) except Exception as e: if e.message.find('could not serialize access due to concurrent update') > -1: raise RetryableJobError('', 30, True) raise e # If the order has been shipped we call to stock.picking.action_done() if binding.order_status_id.name == 'Shipped': for pick in binding.odoo_id.picking_ids: pick.action_done() if binding.order_status_id.name == 'Canceled': for pick in binding.odoo_id.picking_ids: if pick.state != 'done': pick.action_cancel()
def _get_first_price(self, backend, filters): assert filters with backend.work_on(self._name) as work: importer = work.component(usage='amazon.product.price.import') detail = self.env['amazon.product.product.detail'].browse( filters['product_detail']) result = None if filters['method'] == 'first_price': result = importer.run_first_price(detail) elif filters['method'] == 'first_offer': result = importer.run_first_offer(detail) if not result: raise RetryableJobError(msg='The prices can\'t be recovered', seconds=600)
def _check_in_new_connector_env(self): # with self.do_in_new_connector_env() as new_connector_env: with self.do_in_new_connector_env(): # Even when we use an advisory lock, we may have # concurrent issues. # Explanation: # We import Partner A and B, both of them import a # partner category X. # # The squares represent the duration of the advisory # lock, the transactions starts and ends on the # beginnings and endings of the 'Import Partner' # blocks. # T1 and T2 are the transactions. # # ---Time---> # > T1 /------------------------\ # > T1 | Import Partner A | # > T1 \------------------------/ # > T1 /-----------------\ # > T1 | Imp. Category X | # > T1 \-----------------/ # > T2 /------------------------\ # > T2 | Import Partner B | # > T2 \------------------------/ # > T2 /-----------------\ # > T2 | Imp. Category X | # > T2 \-----------------/ # # As you can see, the locks for Category X do not # overlap, and the transaction T2 starts before the # commit of T1. So no lock prevents T2 to import the # category X and T2 does not see that T1 already # imported it. # # The workaround is to open a new DB transaction at the # beginning of each import (e.g. at the beginning of # "Imp. Category X") and to check if the record has been # imported meanwhile. If it has been imported, we raise # a Retryable error so T2 is rollbacked and retried # later (and the new T3 will be aware of the category X # from the its inception). binder = self.binder_for(model=self.model._name) # binder = new_connector_env.get_connector_unit(Binder) if binder.to_internal(self.prestashop_id): raise RetryableJobError( 'Concurrent error. The job will be retried later', seconds=RETRY_WHEN_CONCURRENT_DETECTED, ignore_retry=True)
def server(self): if not self._login_data.is_valid(): raise ChannelConnectorError(_("Invalid Channel Parameters!")) if self._server is None: try: self._server = xmlrpc.client.ServerProxy( self._login_data.address) res, tok = self._server.acquire_token(self._login_data.user, self._login_data.passwd, self._login_data.pkey) if res == 0: self._token = tok else: self._server = None except Exception: self._server = None raise RetryableJobError(_("Can't connect with channel!")) return self._server
def odoo_execute_kw(model, method, *args, **kwargs): log_transmission( "XMLRPC DB={} URL={}".format(params.DB, params.URL), json.dumps([method, args, kwargs]), ) try: common = _client.ServerProxy("{}/xmlrpc/2/common".format( params.URL)) uid = common.authenticate(params.DB, secrets.USERNAME, secrets.PASSWORD, {}) models = _client.ServerProxy("{}/xmlrpc/2/object".format( params.URL)) except OSError: raise RetryableJobError("Error on connecting to external Odoo") res = models.execute_kw(params.DB, uid, secrets.PASSWORD, model, method, args, kwargs) log("Response: %s" % res, level="debug") return res
def _get_report(self, report_list_id): report_api = Reports(backend=self._backend) try: response = report_api.get_report(report_id=report_list_id) if response and response.response and response.response.status_code == 200: return response except MWSError as e: _logger.error("report_api(%s) failed with report_ids %s", '_get_report ', report_list_id) if len(e.args) and e.args[0].lower().index('Request is throttled'): raise RetryableJobError( msg= 'The request to get_report is throttled, we must wait 5 minutes', seconds=300, ignore_retry=True) raise FailedJobError( 'The request to get report failed: report_list_id: %s (%s)', report_list_id, e.args[0] if len(e.args) and e.args[0] else '')
def pick_1st_variant(prods): # NOTE: if the order is changed by adding `asc/desc` this can be broken # but it's very unlikely that the default order for product.product # will be changed. try: ordered = sorted(prods, key=lambda var: [var[x] for x in order_by]) except TypeError as orig_exception: # TypeError: '<' not supported between instances of 'bool' and 'str' # It means we don't have all values to determine this value. msg = _( "Cannot determine main variant for template ID: %s." "\nAt least one variant misses one of these values: %s." "\nWill try again later till 'max retries' count is reached." ) % (prods[0]["tmpl_record_id"], ", ".join(order_by)) # This issue might depend on incomplete state of product info. # Eg: missing translation for variant matching current lang. # Let's retry later a bunch of times (5 by default). raise RetryableJobError(msg) from orig_exception return ordered[0].get("id") if ordered else None
def call(self, method, arguments, http_method=None, storeview=None): try: # When Magento is installed on PHP 5.4+, the API # may return garble data if the arguments contain # trailing None. if isinstance(arguments, list): while arguments and arguments[-1] is None: arguments.pop() start = datetime.now() try: result = self.api_call( method, arguments, http_method=http_method, storeview=storeview) except Exception: _logger.error("api.call('%s', %s) failed", method, arguments) raise else: _logger.debug("api.call('%s', %s) returned %s in %s seconds", method, arguments, result, (datetime.now() - start).seconds) # Uncomment to record requests/responses in ``recorder`` # record(method, arguments, result) return result except (socket.gaierror, socket.error, socket.timeout) as err: raise NetworkRetryableError( 'A network error caused the failure of the job: ' '%s' % err) except xmlrpc.client.ProtocolError as err: if err.errcode in [502, # Bad gateway 503, # Service unavailable 504]: # Gateway timeout raise RetryableJobError( 'A protocol error caused the failure of the job:\n' 'URL: %s\n' 'HTTP/HTTPS headers: %s\n' 'Error code: %d\n' 'Error message: %s\n' % (err.url, err.headers, err.errcode, err.errmsg)) else: raise
def check(self, record): # cannot import this as a dependency since the data source was posted, not imported partner_binder = self.binder_for('magento.res.partner') partner_binding = partner_binder.to_internal(record['CustomerID']) if not partner_binding: raise RetryableJobError( "Couldn't find respective Customer ID - %s " "for the Request ID - %s" % (record['CustomerID'], record['request_id']), seconds=1 * 60) rewards_binder = self.binder_for('magento.res.partner.rewards') rewards_binding = rewards_binder.to_internal(record['CustomerID']) if rewards_binding: existing_date = datetime.strptime( rewards_binding.magento_date_modified, '%Y-%m-%d %H:%M:%S') modified_dt = datetime.strptime(record['ModifiedDate'], '%Y-%m-%d %H:%M:%S') if modified_dt < existing_date: raise NothingToDoJob( "The most recent rewards point already imported with this Request ID - %s.\n" "This is old Request ID - %s. Odoo ID - %s" % (rewards_binding.request_id, record['request_id'], rewards_binding.odoo_id.id))
def import_record(self, backend, external_id): _super = super(AmazonSaleOrder, self) result = _super.import_record(backend, external_id) if not result: raise RetryableJobError("The sale of the backend %s hasn\'t could not be imported.\n %s", backend.name, external_id, 60)
def run(self, external_id, force=False, record=None, **kwargs): """ Run the synchronization A record can be given, reducing number of calls when a call already returns data (example: user returns addresses) :param external_id: identifier of the record on Jira """ self.external_id = external_id lock_name = 'import({}, {}, {}, {})'.format( self.backend_record._name, self.backend_record.id, self.model._name, self.external_id, ) # Keep a lock on this import until the transaction is committed self.advisory_lock_or_retry(lock_name, retry_seconds=RETRY_ON_ADVISORY_LOCK) if record is not None: self.external_record = record else: try: self.external_record = self._get_external_data() except IDMissingInBackend: return self._handle_record_missing_on_jira() binding = self._get_binding() if not binding: with self.do_in_new_work_context() as new_work: # Even when we use an advisory lock, we may have # concurrent issues. # Explanation: # We import Partner A and B, both of them import a # partner category X. # # The squares represent the duration of the advisory # lock, the transactions starts and ends on the # beginnings and endings of the 'Import Partner' # blocks. # T1 and T2 are the transactions. # # ---Time---> # > T1 /------------------------\ # > T1 | Import Partner A | # > T1 \------------------------/ # > T1 /-----------------\ # > T1 | Imp. Category X | # > T1 \-----------------/ # > T2 /------------------------\ # > T2 | Import Partner B | # > T2 \------------------------/ # > T2 /-----------------\ # > T2 | Imp. Category X | # > T2 \-----------------/ # # As you can see, the locks for Category X do not # overlap, and the transaction T2 starts before the # commit of T1. So no lock prevents T2 to import the # category X and T2 does not see that T1 already # imported it. # # The workaround is to open a new DB transaction at the # beginning of each import (e.g. at the beginning of # "Imp. Category X") and to check if the record has been # imported meanwhile. If it has been imported, we raise # a Retryable error so T2 is rollbacked and retried # later (and the new T3 will be aware of the category X # from the its inception). binder = new_work.component(usage='binder') if binder.to_internal(self.external_id): raise RetryableJobError( 'Concurrent error. The job will be retried later', seconds=RETRY_WHEN_CONCURRENT_DETECTED, ignore_retry=True) reason = self.must_skip(force=force) if reason: return reason if not force and self._is_uptodate(binding): return _('Already up-to-date.') self._before_import() # import the missing linked resources self._import_dependencies() self._import(binding, **kwargs)
def calc_price_to_export(self, id_detail, force_change=False): """ Method to change the prices of the detail product :return: """ # If on product detail change_prices is 'yes' # If product detail change_prices is not 'no' and product change_prices is 'yes' # If product detail and product change_prices is not 'no' and backend change_prices is 'yes' detail = self.env['amazon.product.product.detail'].browse(id_detail) we_have_buybox = detail.is_buybox_mine() # We check if we can change prices if force_change or ((detail.change_prices == '1' or detail.product_id.change_prices == '1' or detail.product_id.backend_id.change_prices == '1') and \ (detail.change_prices != '0' and detail.product_id.change_prices != '0' and detail.product_id.backend_id.change_prices != '0')): # We are trying to get the fee of product try: if not detail.last_update_price_date or datetime.strptime( detail.last_update_price_date, '%Y-%m-%d %H:%M:%S' ) < datetime.today() - timedelta(hours=24): importer = self.work.component( usage='amazon.product.price.import') importer.run_update_price(detail) except Exception as e: raise RetryableJobError( msg='An error has been produced on MWS API', seconds=60, ignore_retry=True) change_price = None # If we have the buybox now if we_have_buybox: change_price = self.up_price_with_buybox(detail) # If we have the buybox and we are not get this now, we to try to up the price else: change_price = self.change_price_to_get_buybox(detail) # If we are in this point, we are going to check if the price has been changed, if not we need to check if the price is between the margins, else we are going to update if not change_price: margin_price = detail._get_margin_price( price=detail.price, price_ship=detail.price_ship) margin_min = detail.min_margin or detail.product_id.min_margin or detail.product_id.backend_id.min_margin margin_max = detail.max_margin or detail.product_id.max_margin or detail.product_id.backend_id.max_margin try_price = None if margin_price and margin_price[1] > margin_max: try_price = detail.product_id.odoo_id._calc_amazon_price( backend=detail.product_id.backend_id, margin=margin_max, marketplace=detail.marketplace_id, percentage_fee=detail.percentage_fee or AMAZON_DEFAULT_PERCENTAGE_FEE, ship_price=detail.price_ship) elif margin_price and margin_price[1] < margin_min: try_price = detail.product_id.odoo_id._calc_amazon_price( backend=detail.product_id.backend_id, margin=margin_min, marketplace=detail.marketplace_id, percentage_fee=detail.percentage_fee or AMAZON_DEFAULT_PERCENTAGE_FEE, ship_price=detail.price_ship) if try_price: detail.price = try_price return True
def _add_listing_to_amazon(self, record): if isinstance(record['product_id'], (int, float)): product = self.env['product.product'].browse(record['product_id']) else: product = record['product_id'] marketplaces = record['marketplaces'] if record.get( 'marketplaces') else self.backend_record.marketplace_ids margin = record['margin'] if record.get( 'margin' ) else self.backend_record.max_margin or AMAZON_DEFAULT_PERCENTAGE_MARGIN # Get asin if we have this asin = None if not record.get('asin') and product and product.amazon_bind_ids: asin = product.amazon_bind_ids.asin if len( product.amazon_bind_ids ) < 2 else product.amazon_bind_ids[0].asin else: asin = record['asin'] if record.get('asin') else None product_doesnt_exist = True product_dont_match = False # We get the user language for match with the marketplace language user = self.env['res.users'].browse(self.env.uid) market_lang_match = marketplaces.filtered( lambda marketplace: marketplace.lang_id.code == user.lang) if market_lang_match and not asin: try: amazon_prod = self._get_asin_product(product, market_lang_match) except RetryableJobError as e: raise RetryableJobError('The quota is Throttled', 1800, True) asin = amazon_prod['asin'] if amazon_prod and amazon_prod.get( 'asin') else None product_dont_match = True if amazon_prod and amazon_prod.get( 'Match name') == 'No' else False for marketplace in marketplaces: # If we haven't asin and we haven't searched yet, we search this if not asin and market_lang_match and market_lang_match.id != marketplace.id: try: amazon_prod = self._get_asin_product( product, market_lang_match) except RetryableJobError as e: raise RetryableJobError('The quota is Throttled', 1800, True) asin = amazon_prod['asin'] if amazon_prod and amazon_prod.get( 'asin') else None product_dont_match = True if amazon_prod and amazon_prod.get( 'Match name') == 'No' else False add_product = False if not asin else True if not add_product: continue product_doesnt_exist = False price = product._calc_amazon_price( backend=self.backend_record, margin=margin, marketplace=marketplace, percentage_fee=AMAZON_DEFAULT_PERCENTAGE_FEE) if price: row = {} row['sku'] = product.default_code or product.product_variant_id.default_code row['product-id'] = asin row['product-id-type'] = 'ASIN' price = product._calc_amazon_price( backend=self.backend_record, margin=margin, marketplace=marketplace, percentage_fee=AMAZON_DEFAULT_PERCENTAGE_FEE) row['price'] = ("%.2f" % price).replace( '.', marketplace.decimal_currency_separator) if price else '' row['minimum-seller-allowed-price'] = '' row['maximum-seller-allowed-price'] = '' row['item-condition'] = '11' # We assume the products are new row['quantity'] = '0' # The products stocks allways is 0 when we export these row['add-delete'] = 'a' row['will-ship-internationally'] = '' row['expedited-shipping'] = '' row['merchant-shipping-group-name'] = '' handling_time = product._compute_amazon_handling_time() or '' row['handling-time'] = str(handling_time) if price else '' row['item_weight'] = '' row['item_weight_unit_of_measure'] = '' row['item_volume'] = '' row['item_volume_unit_of_measure'] = '' row['id_mws'] = marketplace.id_mws vals = { 'backend_id': self.backend_record.id, 'type': '_POST_FLAT_FILE_INVLOADER_DATA_', 'model': product._name, 'identificator': product.id, 'data': row, } self.env['amazon.feed.tothrow'].create(vals) if product_doesnt_exist and not product_dont_match: # TODO Create a list of products to create vals = {'product_id': product.product_tmpl_id.id} self.env['amazon.report.product.to.create'].create(vals)