Files
suite/connector_amazon_sp/models/sale_order/common.py
2022-02-04 13:25:45 -08:00

284 lines
11 KiB
Python

# © 2021 Hibou Corp.
import logging
from time import sleep
import odoo.addons.decimal_precision as dp
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action
from odoo.addons.component.core import Component
from odoo.addons.queue_job.exception import RetryableJobError
from ...components.api.amazon import RequestRateError
SO_REQUEST_SLEEP_SECONDS = 30
_logger = logging.getLogger(__name__)
SO_IMPORT_RETRY_PATTERN = {
1: 10 * 60,
2: 30 * 60,
}
class AmazonSaleOrder(models.Model):
_name = 'amazon.sale.order'
_inherit = 'amazon.binding'
_description = 'Amazon Sale Order'
_inherits = {'sale.order': 'odoo_id'}
_order = 'date_order desc, id desc'
odoo_id = fields.Many2one(comodel_name='sale.order',
string='Sale Order',
required=True,
ondelete='cascade')
amazon_order_line_ids = fields.One2many(
comodel_name='amazon.sale.order.line',
inverse_name='amazon_order_id',
string='Amazon Order Lines'
)
total_amount = fields.Float(
string='Total amount',
digits=dp.get_precision('Account')
)
# total_amount_tax = fields.Float(
# string='Total amount w. tax',
# digits=dp.get_precision('Account')
# )
# Ideally would be a selection, but there are/will be more codes we might
# not be able to predict like 'Second US D2D Dom'
# Standard, Expedited, Second US D2D Dom,
fulfillment_channel = fields.Selection([
('AFN', 'Amazon'),
('MFN', 'Merchant'),
], string='Fulfillment Channel')
ship_service_level = fields.Char(string='Shipping Service Level')
ship_service_level_category = fields.Char(string='Shipping Service Level Category')
marketplace = fields.Char(string='Marketplace')
order_type = fields.Char(string='Order Type')
is_business_order = fields.Boolean(string='Is Business Order')
is_prime = fields.Boolean(string='Is Prime')
is_global_express_enabled = fields.Boolean(string='Is Global Express')
is_premium = fields.Boolean(string='Is Premium')
is_sold_by_ab = fields.Boolean(string='Is Sold By AB')
is_amazon_order = fields.Boolean('Is Amazon Order', compute='_compute_is_amazon_order')
def is_fba(self):
return self.fulfillment_channel == 'AFN'
def _compute_is_amazon_order(self):
for so in self:
so.is_amazon_order = True
@job(default_channel='root.amazon')
@api.model
def import_batch(self, backend, filters=None):
""" Prepare the import of Sales Orders from Amazon """
return super(AmazonSaleOrder, self).import_batch(backend, filters=filters)
@job(default_channel='root.amazon', retry_pattern=SO_IMPORT_RETRY_PATTERN)
@related_action(action='related_action_unwrap_binding')
@api.model
def import_record(self, backend, external_id, force=False):
return super().import_record(backend, external_id, force=force)
@api.multi
def action_confirm(self):
res = self.odoo_id.action_confirm()
if res and hasattr(res, '__getitem__'): # Button returned an action: we need to set active_id to the amazon sale order
res.update({
'context': {
'active_id': self.ids[0],
'active_ids': self.ids
}
})
return res
@api.multi
def action_cancel(self):
return self.odoo_id.action_cancel()
@api.multi
def action_draft(self):
return self.odoo_id.action_draft()
@api.multi
def action_view_delivery(self):
res = self.odoo_id.action_view_delivery()
res.update({
'context': {
'active_id': self.ids[0],
'active_ids': self.ids
}
})
return res
# @job(default_channel='root.amazon')
# @api.model
# def acknowledge_order(self, backend, external_id):
# with backend.work_on(self._name) as work:
# adapter = work.component(usage='backend.adapter')
# return adapter.acknowledge_order(external_id)
class SaleOrder(models.Model):
_inherit = 'sale.order'
amazon_bind_ids = fields.One2many(
comodel_name='amazon.sale.order',
inverse_name='odoo_id',
string='Amazon Bindings',
)
amazon_bind_id = fields.Many2one('amazon.sale.order', 'Amazon Binding', compute='_compute_amazon_bind_id')
is_amazon_order = fields.Boolean('Is Amazon Order', compute='_compute_is_amazon_order')
total_amount = fields.Float(
string='Total amount',
digits=dp.get_precision('Account'),
related='amazon_bind_id.total_amount'
)
fulfillment_channel = fields.Selection(related='amazon_bind_id.fulfillment_channel')
ship_service_level = fields.Char(string='Shipping Service Level', related='amazon_bind_id.ship_service_level')
ship_service_level_category = fields.Char(string='Shipping Service Level Category', related='amazon_bind_id.ship_service_level_category')
marketplace = fields.Char(string='Marketplace', related='amazon_bind_id.marketplace')
order_type = fields.Char(string='Order Type', related='amazon_bind_id.order_type')
is_business_order = fields.Boolean(string='Is Business Order', related='amazon_bind_id.is_business_order')
is_prime = fields.Boolean(string='Is Prime', related='amazon_bind_id.is_prime')
is_global_express_enabled = fields.Boolean(string='Is Global Express', related='amazon_bind_id.is_global_express_enabled')
is_premium = fields.Boolean(string='Is Premium', related='amazon_bind_id.is_premium')
is_sold_by_ab = fields.Boolean(string='Is Sold By AB', related='amazon_bind_id.is_sold_by_ab')
@api.depends('amazon_bind_ids')
def _compute_amazon_bind_id(self):
for so in self:
so.amazon_bind_id = so.amazon_bind_ids[:1].id
def _compute_is_amazon_order(self):
for so in self:
so.is_amazon_order = False
# @api.multi
# def action_confirm(self):
# res = super(SaleOrder, self).action_confirm()
# self.amazon_bind_ids.action_confirm()
# return res
class AmazonSaleOrderLine(models.Model):
_name = 'amazon.sale.order.line'
_inherit = 'amazon.binding'
_description = 'Amazon Sale Order Line'
_inherits = {'sale.order.line': 'odoo_id'}
amazon_order_id = fields.Many2one(comodel_name='amazon.sale.order',
string='Amazon Sale Order',
required=True,
ondelete='cascade',
index=True)
odoo_id = fields.Many2one(comodel_name='sale.order.line',
string='Sale Order Line',
required=True,
ondelete='cascade')
backend_id = fields.Many2one(
related='amazon_order_id.backend_id',
string='Amazon Backend',
readonly=True,
store=True,
# override 'Amazon.binding', can't be INSERTed if True:
required=False,
)
@api.model
def create(self, vals):
amazon_order_id = vals['amazon_order_id']
binding = self.env['amazon.sale.order'].browse(amazon_order_id)
vals['order_id'] = binding.odoo_id.id
binding = super(AmazonSaleOrderLine, self).create(vals)
return binding
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
amazon_bind_ids = fields.One2many(
comodel_name='amazon.sale.order.line',
inverse_name='odoo_id',
string="Amazon Bindings",
)
class SaleOrderAdapter(Component):
_name = 'amazon.sale.order.adapter'
_inherit = 'amazon.adapter'
_apply_on = 'amazon.sale.order'
def _api(self):
return self.api_instance.orders()
def search(self, filters):
try:
res = self._api().get_orders(**filters)
if res.errors:
_logger.error('Error in Order search: ' + str(res.errors))
except self.api_instance.SellingApiException as e:
raise ValidationError('SellingApiException: ' + str(e.message))
return res.payload
# Note that order_items_buyer_info has always returned only the order items numbers.
def read(self, order_id,
include_order_items=False,
include_order_address=False,
include_order_buyer_info=False,
include_order_items_buyer_info=False,
):
try:
api = self._api()
order_res = api.get_order(order_id)
if order_res.errors:
_logger.error('Error in Order read: ' + str(order_res.errors))
res = order_res.payload
if include_order_items:
order_items_res = api.get_order_items(order_id)
if order_items_res.errors:
_logger.error('Error in Order Items read: ' + str(order_items_res.errors))
# Note that this isn't the same as the ones below to simplify later code
# by being able to get an iterable at the top level for mapping purposes
res['OrderItems'] = order_items_res.payload.get('OrderItems', [])
if include_order_address:
order_address_res = api.get_order_address(order_id)
if order_address_res.errors:
_logger.error('Error in Order Address read: ' + str(order_address_res.errors))
res['OrderAddress'] = order_address_res.payload
if include_order_buyer_info:
order_buyer_info_res = api.get_order_buyer_info(order_id)
if order_buyer_info_res.errors:
_logger.error('Error in Order Buyer Info read: ' + str(order_buyer_info_res.errors))
res['OrderBuyerInfo'] = order_buyer_info_res.payload
if include_order_items_buyer_info:
order_items_buyer_info_res = api.get_order_items_buyer_info(order_id)
if order_items_buyer_info_res.errors:
_logger.error('Error in Order Items Buyer Info read: ' + str(order_items_buyer_info_res.errors))
res['OrderItemsBuyerInfo'] = order_items_buyer_info_res.payload
except self.api_instance.SellingApiException as e:
if e.message.find('You exceeded your quota for the requested resource.') >= 0:
self._sleep_rety()
raise ValidationError('SellingApiException: ' + str(e.message))
except RequestRateError as e:
self._sleep_rety()
return res
def _sleep_rety(self):
# we CANNOT control when the next job of this type will be scheduled (by def, the queue may even be running
# the same jobs at the same time)
# we CAN control how long we wait before we free up the current queue worker though...
# Note that we can make it so that this job doesn't re-queue right away via RetryableJobError mechanisms,
# but that is no better than the more general case of us just sleeping this long now.
_logger.warn(' !!!!!!!!!!!!! _sleep_rety !!!!!!!!!!!!')
sleep(SO_REQUEST_SLEEP_SECONDS)
raise RetryableJobError('We are being throttled and will retry later.')