[REL] connector_amazon_sp: for 11.0

This commit is contained in:
Jared Kipe
2022-02-04 13:25:45 -08:00
parent 8b2afa882d
commit bf7192f71a
48 changed files with 4264 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# © 2021 Hibou Corp.
from . import test_orders
from . import test_product_listing

View File

@@ -0,0 +1,88 @@
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from sp_api.base.ApiResponse import ApiResponse
from unittest.mock import patch
@contextmanager
def mock_submit_feed_api(return_error=False):
submit_feed_res1 = {'errors': None,
'headers': {'Content-Length': '665',
'Content-Type': 'application/json',
'Date': datetime.now(tz=timezone.utc).strftime('%a, %d %b %Y %H:%M:00 %Z'),
},
'kwargs': {},
'next_token': None,
'pagination': None,
'payload': {'encryptionDetails': {'initializationVector': '',
'key': '',
'standard': 'AES'},
'feedDocumentId': '',
'url': ''}}
submit_feed_res2 = {'errors': None,
'headers': {'Content-Length': '37',
'Content-Type': 'application/json',
'Date': datetime.now(tz=timezone.utc).strftime('%a, %d %b %Y %H:%M:00 %Z'),
},
'kwargs': {},
'next_token': None,
'pagination': None,
'payload': {'feedId': '555555555555'}}
if return_error:
submit_feed_res2['payload'] = {}
with patch('odoo.addons.connector_amazon_sp.components.api.amazon.Feeds') as mock_feeds:
mock_feeds.return_value.submit_feed.return_value = ApiResponse(**submit_feed_res1), ApiResponse(**submit_feed_res2)
yield
@contextmanager
def mock_check_feed_api(done=False):
check_feed_res3 = {'errors': None,
'headers': {'Content-Length': '175', 'Content-Type': 'application/json',
'Date': datetime.now(tz=timezone.utc).strftime('%a, %d %b %Y %H:%M:00 %Z')},
'kwargs': {},
'next_token': None,
'pagination': None,
'payload': {'createdTime': datetime.now(tz=timezone.utc).isoformat(timespec='seconds'),
'feedId': '555555555555',
'feedType': 'POST_PRODUCT_DATA',
'marketplaceIds': ['555555555555'],
'processingStatus': 'IN_QUEUE'}}
if done:
check_feed_res3['payload']['processingStatus'] = 'DONE'
end_time = datetime.now(tz=timezone.utc)
start_time = end_time - timedelta(minutes=2)
check_feed_res3['payload']['processingStartTime'] = start_time.isoformat(timespec='seconds')
check_feed_res3['payload']['processingEndTime'] = end_time.isoformat(timespec='seconds')
check_feed_res3['payload']['resultFeedDocumentId'] = 'xxxxxxxx'
feed_result_document = """
<?xml version="1.0" encoding="UTF-8"?>
<AmazonEnvelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="amzn-envelope.xsd">
<Header>
<DocumentVersion>1.02</DocumentVersion>
<MerchantIdentifier>555555555555</MerchantIdentifier>
</Header>
<MessageType>ProcessingReport</MessageType>
<Message>
<MessageID>1</MessageID>
<ProcessingReport>
<DocumentTransactionID>555555555555</DocumentTransactionID>
<StatusCode>Complete</StatusCode>
<ProcessingSummary>
<MessagesProcessed>1</MessagesProcessed>
<MessagesSuccessful>1</MessagesSuccessful>
<MessagesWithError>0</MessagesWithError>
<MessagesWithWarning>0</MessagesWithWarning>
</ProcessingSummary>
</ProcessingReport>
</Message>
</AmazonEnvelope>
"""
with patch('odoo.addons.connector_amazon_sp.components.api.amazon.Feeds') as mock_feeds:
mock_feeds.return_value.get_feed.return_value = ApiResponse(**check_feed_res3)
mock_feeds.return_value.get_feed_result_document.return_value = feed_result_document
yield

View File

@@ -0,0 +1,74 @@
from contextlib import contextmanager
from sp_api.base.ApiResponse import ApiResponse
from unittest.mock import patch
get_order_response = {'payload': {'AmazonOrderId': '111-1111111-1111111',
'PurchaseDate': '2021-04-24T20:22:03Z',
'LastUpdateDate': '2021-04-26T17:25:41Z',
'OrderStatus': 'Shipped',
'FulfillmentChannel': 'MFN',
'SalesChannel': 'Amazon.com',
'ShipServiceLevel': 'Std US D2D Dom',
'OrderTotal': {'CurrencyCode': 'USD',
'Amount': '159.96'},
'NumberOfItemsShipped': 1,
'NumberOfItemsUnshipped': 0,
'PaymentMethod': 'Other',
'PaymentMethodDetails': ['Standard'],
'IsReplacementOrder': False,
'MarketplaceId': 'ATVPDKIKX0DER',
'ShipmentServiceLevelCategory': 'Standard',
'OrderType': 'StandardOrder',
'EarliestShipDate': '2021-04-26T07:00:00Z',
'LatestShipDate': '2021-04-27T06:59:59Z',
'EarliestDeliveryDate': '2021-04-30T07:00:00Z',
'LatestDeliveryDate': '2021-05-01T06:59:59Z',
'IsBusinessOrder': False,
'IsPrime': True,
'IsGlobalExpressEnabled': False,
'IsPremiumOrder': False,
'IsSoldByAB': False,
'DefaultShipFromLocationAddress': {'Name': 'null',
'AddressLine1': 'null',
'AddressLine2': 'null',
'City': 'SELLERSBURG',
'StateOrRegion': 'IN',
'PostalCode': '47172',
'CountryCode': 'US'},
'IsISPU': False}}
get_order_items_response = {'payload': {'AmazonOrderId': '111-1111111-1111111',
'OrderItems': [
{'ASIN': 'A1B1C1D1E1',
'OrderItemId': '12345678901234',
'SellerSKU': 'TEST_PRODUCT',
'Title': 'Test Product Purchased From Amazon',
'QuantityOrdered': 1, 'QuantityShipped': 1,
'ProductInfo': {'NumberOfItems': '1'},
'ItemPrice': {'CurrencyCode': 'USD', 'Amount': '199.95'},
'ItemTax': {'CurrencyCode': 'USD', 'Amount': '0.00'},
'PromotionDiscount': {'CurrencyCode': 'USD', 'Amount': '39.99'},
'PromotionDiscountTax': {'CurrencyCode': 'USD', 'Amount': '0.00'},
'PromotionIds': ['Coupon'],
'IsGift': 'false',
'ConditionId': 'New',
'ConditionSubtypeId': 'New',
'IsTransparency': False}]}}
get_order_address_response = {'payload': {'AmazonOrderId': '111-1111111-1111111',
'ShippingAddress': {'StateOrRegion': 'FL', 'PostalCode': '34655-5649',
'City': 'NEW PORT RICHEY', 'CountryCode': 'US',
'Name': ''}}}
get_order_buyer_info_response = {'payload': {'AmazonOrderId': '111-1111111-1111111',
'BuyerEmail': 'obfuscated@marketplace.amazon.com'}}
@contextmanager
def mock_orders_api():
with patch('odoo.addons.connector_amazon_sp.components.api.amazon.Orders') as mock_orders:
mock_orders.return_value.get_order.return_value = ApiResponse(**get_order_response)
mock_orders.return_value.get_order_items.return_value = ApiResponse(**get_order_items_response)
mock_orders.return_value.get_order_address.return_value = ApiResponse(**get_order_address_response)
mock_orders.return_value.get_order_buyer_info.return_value = ApiResponse(**get_order_buyer_info_response)
yield

View File

@@ -0,0 +1,39 @@
# © 2021 Hibou Corp.
from odoo.addons.component.tests.common import SavepointComponentCase
import odoo
class AmazonTestCase(SavepointComponentCase):
""" Base class - Test the imports from a Amazon Mock. """
def setUp(self):
super(AmazonTestCase, self).setUp()
# disable commits when run from pytest/nosetest
odoo.tools.config['test_enable'] = True
# We need a backend configured in the db to avoid storing credentials
self.backend = self.env['amazon.backend'].create({
'name': 'Test',
'api_refresh_token': 'Not null',
'api_lwa_client_id': 'Not null',
'api_lwa_client_secret': 'Not null',
'api_aws_access_key': 'Not Null',
'api_aws_secret_key': 'Not Null',
'api_role_arn': 'Not Null',
'merchant_id': 'Test Merchant ID',
'payment_mode_id': self.browse_ref('account_payment_mode.payment_mode_inbound_ct1').id,
'product_categ_id': self.browse_ref('product.product_category_1').id,
'sale_prefix': 'TEST',
})
def _import_record(self, model_name, amazon_id):
assert model_name.startswith('amazon.')
self.env[model_name].import_record(self.backend, amazon_id)
binding = self.env[model_name].search(
[('backend_id', '=', self.backend.id),
('external_id', '=', str(amazon_id))]
)
self.assertEqual(len(binding), 1)
return binding

View File

@@ -0,0 +1,67 @@
# © 2021 Hibou Corp.
from .api.orders import mock_orders_api
from .common import AmazonTestCase
class TestSaleOrder(AmazonTestCase):
def _import_sale_order(self, amazon_id):
with mock_orders_api():
return self._import_record('amazon.sale.order', amazon_id)
def test_import_sale_order(self):
""" Import sale order and test workflow"""
amazon_order_number = '111-1111111-1111111'
binding = self._import_sale_order(amazon_order_number)
# binding.external_id will be what we pass to import_record regardless of what the API returned
self.assertEqual(binding.external_id, amazon_order_number)
self.assertTrue(binding.is_amazon_order)
self.assertFalse(binding.odoo_id.is_amazon_order)
self.assertEqual(binding.effective_date, False) # This is a computed field, should it be in the mapper?
self.assertEqual(binding.date_planned, '2021-04-27 06:59:59')
self.assertEqual(binding.requested_date, '2021-05-01 06:59:59')
self.assertEqual(binding.ship_service_level, 'Std US D2D Dom')
self.assertEqual(binding.ship_service_level_category, 'Standard')
self.assertEqual(binding.marketplace, 'ATVPDKIKX0DER')
self.assertEqual(binding.order_type, 'StandardOrder')
self.assertFalse(binding.is_business_order)
self.assertTrue(binding.is_prime)
self.assertFalse(binding.is_global_express_enabled)
self.assertFalse(binding.is_premium)
self.assertFalse(binding.is_sold_by_ab)
self.assertEqual(binding.name, 'TEST' + amazon_order_number)
self.assertAlmostEqual(binding.total_amount, 159.96)
self.assertEqual(binding.currency_id, self.browse_ref('base.USD'))
default_warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1)
self.assertEqual(binding.warehouse_id, default_warehouse)
self.assertEqual(binding.payment_mode_id, self.browse_ref('account_payment_mode.payment_mode_inbound_ct1'))
self.assertEqual(len(binding.amazon_order_line_ids), 1)
self._test_import_sale_order_line(binding.amazon_order_line_ids[0])
self.assertEqual(binding.state, 'draft')
binding.action_confirm()
self.assertEqual(binding.state, 'sale')
self.assertEqual(binding.delivery_count, 1)
binding.action_cancel()
self.assertEqual(binding.state, 'cancel')
binding.action_draft()
self.assertEqual(binding.state, 'draft')
def _test_import_sale_order_line(self, binding_line):
self.assertEqual(binding_line.external_id, '12345678901234')
self.assertEqual(binding_line.name, 'Test Product Purchased From Amazon')
self.assertEqual(binding_line.product_uom_qty, 1)
self.assertAlmostEqual(binding_line.price_unit, 199.95)
self.assertAlmostEqual(binding_line.discount, 20.0)
product = binding_line.product_id
self.assertEqual(product.default_code, 'TEST_PRODUCT')
self.assertEqual(product.name, 'Test Product Purchased From Amazon')
self.assertAlmostEqual(product.list_price, 199.95)
self.assertEqual(product.categ_id, self.browse_ref('product.product_category_1'))
product_binding = product.amazon_bind_ids[0]
self.assertEqual(product_binding.external_id, product.default_code)
self.assertEqual(product_binding.asin, 'A1B1C1D1E1')

View File

@@ -0,0 +1,179 @@
# © 2021 Hibou Corp.
from base64 import b64decode
from datetime import date, datetime, timedelta
from xml.etree import ElementTree
from .api.feeds import mock_submit_feed_api, mock_check_feed_api
from .common import AmazonTestCase
from odoo.addons.queue_job.exception import RetryableJobError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from odoo import fields
class TestProductListing(AmazonTestCase):
def setUp(self):
super(TestProductListing, self).setUp()
self.product = self.browse_ref('stock.product_icecream')
self.amazon_product = self.env['amazon.product.product'].create({
'external_id': 'Amazon Ice Cream',
'odoo_id': self.product.id,
'backend_id': self.backend.id,
'asin': '',
'lst_price': 12.99,
})
def test_00_create_feed(self):
self.assertEqual(self.amazon_product.state, 'draft')
self.amazon_product.button_submit_product()
self.assertEqual(self.amazon_product.state, 'sent')
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
merchant_id = root.find('./Header/MerchantIdentifier').text
self.assertEqual(merchant_id, self.backend.merchant_id)
product_elem = root.find('./Message/Product')
self.assertEqual(product_elem.find('SKU').text, self.amazon_product.external_id)
with mock_submit_feed_api(return_error=True):
feed.submit_feed()
self.assertEqual(feed.state, 'error_on_submit')
with mock_submit_feed_api():
feed.submit_feed()
self.assertEqual(feed.state, 'submitted')
self.assertEqual(feed.external_id, '555555555555')
with mock_check_feed_api():
with self.assertRaises(RetryableJobError):
feed.check_feed()
self.assertEqual(feed.amazon_state, 'IN_QUEUE')
with mock_check_feed_api(done=True):
feed.check_feed()
self.assertEqual(feed.amazon_state, 'DONE')
def test_10_update_inventory(self):
stock_location = self.env.ref('stock.stock_location_stock')
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': stock_location.id,
'quantity': 7.0,
})
self.assertFalse(self.amazon_product.date_inventory_sent)
self.amazon_product.button_update_inventory()
self.assertEqual(fields.Datetime.from_string(self.amazon_product.date_inventory_sent).date(), date.today())
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
inventory_elem = root.find('./Message/Inventory')
self.assertEqual(inventory_elem.find('SKU').text, self.amazon_product.external_id)
self.assertEqual(float(inventory_elem.find('Quantity').text), 7.0)
def test_11_update_inventory_global_buffer(self):
test_qty = 7.0
global_buffer = 2.0
self.backend.buffer_qty = global_buffer
stock_location = self.env.ref('stock.stock_location_stock')
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': stock_location.id,
'quantity': test_qty,
})
self.assertFalse(self.amazon_product.date_inventory_sent)
self.amazon_product.button_update_inventory()
self.assertEqual(fields.Datetime.from_string(self.amazon_product.date_inventory_sent).date(), date.today())
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
inventory_elem = root.find('./Message/Inventory')
self.assertEqual(inventory_elem.find('SKU').text, self.amazon_product.external_id)
self.assertEqual(float(inventory_elem.find('Quantity').text), test_qty - global_buffer)
def test_12_update_inventory_listing_buffer(self):
test_qty = 7.0
global_buffer = 2.0
product_buffer = 3.0
self.backend.buffer_qty = global_buffer
self.amazon_product.buffer_qty = product_buffer
stock_location = self.env.ref('stock.stock_location_stock')
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': stock_location.id,
'quantity': test_qty,
})
self.assertFalse(self.amazon_product.date_inventory_sent)
self.amazon_product.button_update_inventory()
self.assertEqual(fields.Datetime.from_string(self.amazon_product.date_inventory_sent).date(), date.today())
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
inventory_elem = root.find('./Message/Inventory')
self.assertEqual(inventory_elem.find('SKU').text, self.amazon_product.external_id)
self.assertEqual(float(inventory_elem.find('Quantity').text), test_qty - product_buffer)
def test_20_update_price_no_pricelist(self):
self.assertFalse(self.amazon_product.date_price_sent)
self.amazon_product.button_update_price()
self.assertEqual(fields.Datetime.from_string(self.amazon_product.date_price_sent).date(), date.today())
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
price_elem = root.find('./Message/Price')
self.assertEqual(price_elem.find('SKU').text, self.amazon_product.external_id)
self.assertEqual(float(price_elem.find('StandardPrice').text), 12.99)
self.assertIsNone(price_elem.find('SalePrice'))
def test_30_update_price_with_pricelist(self):
today = date.today()
yesterday = today - timedelta(days=1)
tomorrow = today + timedelta(days=1)
self.backend.pricelist_id = self.env['product.pricelist'].create({
'name': 'Test Pricelist',
'item_ids': [(0, 0, {
'applied_on': '1_product',
'product_tmpl_id': self.product.product_tmpl_id.id,
'compute_price': 'fixed',
'fixed_price': 9.99,
'date_start': yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT),
'date_end': tomorrow.strftime(DEFAULT_SERVER_DATE_FORMAT),
})],
})
self.amazon_product.button_update_price()
feed = self.env['amazon.feed'].search([('amazon_product_product_id', '=', self.amazon_product.id)])
self.assertEqual(len(feed), 1)
self.assertEqual(feed.state, 'new')
self.assertEqual(feed.amazon_state, 'not_sent')
feed_contents = b64decode(feed.data).decode('iso-8859-1')
root = ElementTree.fromstring(feed_contents)
price_elem = root.find('./Message/Price')
self.assertEqual(price_elem.find('SKU').text, self.amazon_product.external_id)
self.assertEqual(float(price_elem.find('StandardPrice').text), 12.99)
sale_elem = price_elem.find('./Sale')
self.assertEqual(float(sale_elem.find('SalePrice').text), 9.99)
self.assertEqual(sale_elem.find('StartDate').text, datetime(yesterday.year, yesterday.month, yesterday.day).isoformat())
self.assertEqual(sale_elem.find('EndDate').text, datetime(tomorrow.year, tomorrow.month, tomorrow.day).isoformat())