mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
284 lines
11 KiB
Python
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.')
|