Files

1099 lines
51 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
import json
from unittest.mock import Mock, patch
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests.common import tagged
from odoo.tools import mute_logger
from odoo.addons.sale_amazon import utils as amazon_utils
from odoo.addons.sale_amazon.tests import common
@tagged('post_install', '-at_install')
class TestAmazon(common.TestAmazonCommon):
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_update_marketplaces_no_change(self):
""" Test the available marketplaces synchronization with no change. """
marketplaces = self.env['amazon.marketplace'].search([])
with patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount'
'._get_available_marketplaces',
return_value=marketplaces,
):
self.account.write({
'available_marketplace_ids': [(6, 0, marketplaces.ids)],
'active_marketplace_ids': [(6, 0, marketplaces.ids)],
})
self.account.action_update_available_marketplaces()
self.assertEqual(self.account.available_marketplace_ids.ids, marketplaces.ids)
self.assertEqual(self.account.active_marketplace_ids.ids, marketplaces.ids)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_update_marketplaces_remove(self):
""" Test the available marketplaces synchronization with a marketplace removal. """
marketplaces = self.env['amazon.marketplace'].search([], limit=2)
with patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount'
'._get_available_marketplaces',
return_value=marketplaces[:1],
):
self.account.write({
'available_marketplace_ids': [(6, 0, marketplaces.ids)],
'active_marketplace_ids': [(6, 0, marketplaces.ids)],
})
self.account.action_update_available_marketplaces()
self.assertEqual(self.account.available_marketplace_ids.ids, marketplaces.ids[:1])
self.assertEqual(
self.account.active_marketplace_ids.ids,
marketplaces.ids[:1],
msg="Unavailable marketplaces should be removed from the list of active "
"marketplaces.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_update_marketplaces_add(self):
""" Test the available marketplaces synchronization with a new marketplace. """
marketplaces = self.env['amazon.marketplace'].search([], limit=2)
with patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount'
'._get_available_marketplaces',
return_value=marketplaces,
):
self.account.write({
'available_marketplace_ids': [(6, 0, marketplaces.ids[:1])],
'active_marketplace_ids': [(6, 0, marketplaces.ids[:1])],
})
self.account.action_update_available_marketplaces()
self.assertEqual(self.account.available_marketplace_ids.ids, marketplaces.ids)
self.assertEqual(
self.account.active_marketplace_ids.ids,
marketplaces.ids,
msg="New available marketplaces should be added to the list of active marketplaces"
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_full(self):
""" Test the orders synchronization with on-the-fly creation of all required records. """
def find_matching_product_mock(
_self, product_code_, _default_xmlid, default_name_, default_type_
):
""" Return a product created on-the-fly with the product code as internal reference. """
product_ = self.env['product.product'].create({
'name': default_name_,
'type': default_type_,
'list_price': 0.0,
'sale_ok': False,
'purchase_ok': False,
'default_code': product_code_,
})
product_.product_tmpl_id.taxes_id = [Command.clear()]
return product_
# Create a warehouse that is prioritized when creating a normal order
view_location = self.env['stock.location'].create({
'name': "some location", 'usage': 'view', 'company_id': self.env.company.id
})
stock_location = self.env['stock.location'].create({
'name': "some location", 'usage': 'internal', 'company_id': self.env.company.id
})
other_warehouse = self.env['stock.warehouse'].create({
'name': "Other warehouse",
'code': "OW",
'view_location_id': view_location.id,
'lot_stock_id': stock_location.id,
'sequence': 1,
})
self.assertNotEqual(self.account.location_id.warehouse_id, other_warehouse)
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda _account, operation, **kwargs: common.OPERATIONS_RESPONSES_MAP[operation],
), patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount._recompute_subtotal',
new=lambda self_, subtotal_, *args_, **kwargs_: subtotal_,
), patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount._find_matching_product',
new=find_matching_product_mock,
):
self.account._sync_orders(auto_commit=False)
self.assertEqual(
self.account.last_orders_sync,
datetime(2020, 1, 1),
msg="The last_order_sync should be equal to the date returned by get_orders_data "
"when the synchronization is completed."
)
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
self.assertEqual(len(order), 1, msg="There should have been exactly one order created.")
self.assertEqual(order.origin, 'Amazon Order 123456789')
self.assertEqual(order.date_order, datetime(1378, 4, 8))
self.assertEqual(order.company_id.id, self.account.company_id.id)
self.assertEqual(order.user_id.id, self.account.user_id.id)
self.assertEqual(order.team_id.id, self.account.team_id.id)
self.assertNotEqual(order.warehouse_id.id, self.account.location_id.warehouse_id.id)
self.assertEqual(order.amazon_channel, 'fbm')
self.assertEqual(order.currency_id.name, 'USD')
order_lines = self.env['sale.order.line'].search([('order_id', '=', order.id)])
self.assertEqual(
len(order_lines),
4,
msg="There should have been four order lines created: one for the product, one for "
"the gift wrapping charges, one (note) for the gift message and one for the "
"shipping."
)
product_line = order_lines.filtered(lambda l: l.product_id.default_code == 'TEST')
self.assertEqual(
product_line.price_unit,
50.0,
msg="The unitary price should be the quotient of the item price (tax excluded) "
"divided by the quantity.",
)
self.assertEqual(product_line.discount, 5) # 5% discount.
self.assertEqual(product_line.product_uom_qty, 2.0)
self.assertEqual(product_line.amazon_item_ref, '987654321')
self.assertTrue(product_line.amazon_offer_id)
shipping_line = order_lines.filtered(
lambda l: l.product_id.default_code == 'SHIPPING-CODE'
)
self.assertEqual(shipping_line.price_unit, 12.5)
self.assertEqual(shipping_line.discount, 20) # 2.5/12.5*100
self.assertEqual(shipping_line.product_uom_qty, 1.0)
self.assertFalse(shipping_line.amazon_item_ref)
self.assertFalse(shipping_line.amazon_offer_id)
gift_wrapping_line = order_lines.filtered(
lambda l: l.product_id.default_code == 'WRAP-CODE'
)
self.assertEqual(gift_wrapping_line.price_unit, 3.33)
self.assertEqual(gift_wrapping_line.product_uom_qty, 1.0)
self.assertFalse(gift_wrapping_line.amazon_item_ref)
self.assertFalse(gift_wrapping_line.amazon_offer_id)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_partial(self):
""" Test the orders synchronization interruption with API throttling. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mocked response without making an actual call to the SP-API. """
base_response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
if operation_ == 'searchOrders':
self.search_orders_count += 1
# Page 1: Return the base mock and include a pagination token to trigger a second
# API call.
if self.search_orders_count == 1:
return {**base_response_, "paginationToken": "dummy_token_for_page_2"}
# Page > 1: Simulate the API rate limit hitting on the subsequent request.
raise amazon_utils.AmazonRateLimitError(operation_)
return base_response_
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
):
self.search_orders_count = 0
self.account._sync_orders(auto_commit=False)
self.assertEqual(
self.account.last_orders_sync,
datetime(2017, 1, 20),
msg="The last_order_sync should be equal to the LastUpdateDate of the last fully "
"synchronized order if no all orders could be synchronized.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_fail(self):
""" Test the orders synchronization cancellation with API throttling. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mocked response or raise an AmazonRateLimitError without making an actual
call to the SP-API. """
if operation_ != 'searchOrders':
return common.OPERATIONS_RESPONSES_MAP[operation_]
else:
raise amazon_utils.AmazonRateLimitError(operation_)
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
):
last_order_sync_copy = self.account.last_orders_sync
self.account._sync_orders(auto_commit=False)
self.assertEqual(
self.account.last_orders_sync,
last_order_sync_copy,
msg="The last_order_sync field should not have been modified if the rate limit of "
"one operation was reached when synchronizing an order.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_abort(self):
""" Test the orders synchronization cancellation with no active marketplace. """
last_order_sync_copy = self.account.last_orders_sync
self.account.active_marketplace_ids = False
with self.assertRaises(UserError):
self.account._sync_orders(auto_commit=False)
self.assertEqual(
self.account.last_orders_sync,
last_order_sync_copy,
msg="The last_order_sync field should not have been modified if there is no active "
"marketplace selected for the account.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_fba(self):
""" Test the orders synchronization with Fulfillment By Amazon. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mocked response without making an actual call to the SP-API. """
base_response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
if operation_ == 'searchOrders':
order_mock = base_response_["orders"][0]
return {
**base_response_,
"orders": [
{
**order_mock,
"fulfillment": {
"fulfillmentStatus": "SHIPPED",
"fulfilledBy": "AMAZON",
},
}
],
}
return base_response_
def find_matching_product_mock(
_self, product_code_, _default_xmlid, default_name_, default_type_
):
""" Return a product created on-the-fly with the product code as internal reference. """
product_ = self.env['product.product'].create({
'name': default_name_,
'type': default_type_,
'list_price': 0.0,
'sale_ok': False,
'purchase_ok': False,
'default_code': product_code_,
})
product_.product_tmpl_id.taxes_id = [Command.clear()]
return product_
# Create a warehouse that is prioritized when creating a normal order
view_location = self.env['stock.location'].create({
'name': "some location", 'usage': 'view', 'company_id': self.env.company.id
})
stock_location = self.env['stock.location'].create({
'name': "some location", 'usage': 'internal', 'company_id': self.env.company.id
})
other_warehouse = self.env['stock.warehouse'].create({
'name': "Other warehouse",
'code': "OW",
'view_location_id': view_location.id,
'lot_stock_id': stock_location.id,
'sequence': 1,
})
self.assertNotEqual(self.account.location_id.warehouse_id, other_warehouse)
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
), patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount._find_matching_product',
new=find_matching_product_mock,
):
self.account._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
self.assertEqual(order.amazon_channel, 'fba')
self.assertEqual(order.warehouse_id.id, self.account.location_id.warehouse_id.id)
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.assertEqual(len(picking), 0, msg="FBA orders should generate no picking.")
products = order.order_line.mapped('product_id').filtered(lambda p: p.type != 'service')
moves = self.env['stock.move'].search([('product_id', 'in', products.ids)])
self.assertEqual(
len(moves),
len(products),
msg="FBA orders should generate one stock move per product that is not a service.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
def test_sync_orders_cancel(self):
""" Test the orders synchronization with cancellation from Amazon. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mocked response without making an actual call to the SP-API. """
base_response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
order_status_ = 'UNSHIPPED' if not order_created else 'CANCELLED'
if operation_ == 'searchOrders':
order_mock = base_response_['orders'][0]
return {
**base_response_,
"orders": [
{
**order_mock,
"fulfillment": {
**order_mock["fulfillment"],
"fulfillmentStatus": order_status_,
},
}
],
}
return base_response_
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
):
# Sync an order created on Amazon.
order_created = False
self.account._sync_orders(auto_commit=False)
# Sync an order canceled from Amazon.
order_created = True
self.account._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
self.assertEqual(
order.state,
'cancel',
msg="The cancellation of orders should be synchronized from Amazon.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
@mute_logger('odoo.addons.sale_amazon.models.stock_picking')
def test_sync_orders_cancel_abort(self):
""" Test the pickings that were confirmed at odoo and then order is canceled at Amazon. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mock response without making an actual call to the Selling Partner API. """
base_response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
order_status_ = 'UNSHIPPED' if not self.order_canceled else 'CANCELLED'
if operation_ == 'searchOrders':
order_mock = base_response_["orders"][0]
return {
**base_response_,
"orders": [
{
**order_mock,
"fulfillment": {
**order_mock["fulfillment"],
"fulfillmentStatus": order_status_,
},
}
],
}
return base_response_
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
):
# Sync an order created on Amazon.
self.order_canceled = False
self.account._sync_orders(auto_commit=False)
# Check the order state and validate the picking.
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
self.assertNotEqual(order.state, 'cancel')
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
picking.move_ids.picked = True
picking.carrier_id, picking.carrier_tracking_ref = self.carrier, self.tracking_ref
picking._action_done()
self.assertEqual(picking.state, 'done')
# Sync an order canceled from Amazon.
self.order_canceled = True
self.account._sync_orders(auto_commit=False)
def test_sync_orders_replacement(self):
""" Test handling of Amazon replacement orders without currency. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
"""Return a mocked response without making an actual call to the SP-API."""
base_response_ = common.OPERATIONS_RESPONSES_MAP.get(operation_)
if operation_ == "searchOrders":
order_mock = base_response_["orders"][0]
order_items = order_mock["orderItems"][0]
return {
**base_response_,
"orders": [
{
**order_mock,
"associatedOrders": [
{
"orderId": "replaced_order",
"associationType": "REPLACEMENT_ORIGINAL_ID",
}
],
"proceeds": {
# No currencyCode
},
"orderItems": [
{
**order_items,
"orderItemId": "987654321",
"quantityOrdered": 2,
"product": {
**order_items.get("product", {}),
"sellerSku": "TEST",
"title": "Run Test, Run!",
},
"proceeds": {
**order_items.get("proceeds", {}),
"breakdowns": [
{"type": "ITEM", "subtotal": {"amount": "0"}},
{"type": "SHIPPING", "subtotal": {"amount": "0"}},
],
},
}
],
}
],
}
return base_response_
def find_matching_product_mock(
_self, product_code_, _default_xmlid, default_name_, default_type_
):
""" Return a product created on-the-fly with the product code as internal reference. """
product_ = self.env['product.product'].create({
'name': default_name_,
'type': default_type_,
'list_price': 10.0,
'sale_ok': False,
'purchase_ok': False,
'default_code': product_code_,
})
product_.product_tmpl_id.taxes_id = False
return product_
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
), patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount._recompute_subtotal',
new=lambda self_, subtotal_, *args_, **kwargs_: subtotal_,
), patch(
'odoo.addons.sale_amazon.models.amazon_account.AmazonAccount._find_matching_product',
new=find_matching_product_mock,
):
partner = self.env['res.partner'].create({'name': 'Gederic Frilson'})
currency = self.env['res.currency'].create({'name': 'QUA', 'symbol': 'Q'})
pricelist = self.env['product.pricelist'].create({
'name': 'QUA pricelist', 'currency_id': currency.id,
})
self.env['sale.order'].create({
'partner_id': partner.id,
'pricelist_id': pricelist.id,
'amazon_order_ref': 'replaced_order',
})
self.account._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
self.assertEqual(order.currency_id.name, 'QUA')
self.assertEqual(order.amount_total, 0)
def test_inventory_sync_is_skipped_when_disabled(self):
""" Test that the inventory synchronization is skipped when the account has disabled it. """
self.account.synchronize_inventory = False
with patch(
'odoo.addons.sale_amazon.utils.submit_feed', return_value='An_amazing_id'
) as mock:
self.assertEqual(self.account.offer_ids, self.offer)
self.assertEqual(self.offer.amazon_sync_status, False)
self.account._sync_inventory()
self.assertEqual(
mock.call_count,
0,
msg="The stock synchronization is deactivated, no call should have been made.",
)
self.assertEqual(self.offer.amazon_sync_status, False)
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
@mute_logger('odoo.addons.sale_amazon.models.amazon_offer')
def test_sync_inventory(self):
""" Test the inventory availability confirmation synchronization. """
self.account.synchronize_inventory = True
with patch(
'odoo.addons.sale_amazon.utils.submit_feed', return_value='An_amazing_id'
) as mock:
self.assertEqual(self.account.offer_ids, self.offer)
self.assertEqual(self.offer.amazon_sync_status, False)
self.account._sync_inventory()
self.assertEqual(self.offer.amazon_sync_status, 'processing')
self.assertEqual(
mock.call_count,
1,
msg="An inventory availability feed should be sent to Amazon for all the offers.",
)
@mute_logger('odoo.addons.sale_amazon.models.amazon_offer')
def test_inventory_sync_is_skipped_if_loading_feed_data_fails(self):
self.offer.amazon_feed_ref = 'incorrect json' # force fetch of feed data
self.offer.amazon_sync_status = False
with (
patch(
'odoo.addons.sale_amazon.utils.submit_feed', return_value='An_amazing_id'
) as submit_feed_mock,
patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
side_effect=amazon_utils.AmazonRateLimitError(''),
) as make_sp_api_request_mock,
):
self.account._sync_inventory()
make_sp_api_request_mock.assert_called_once()
self.assertEqual(self.offer.amazon_sync_status, 'error')
def test_offer_get_feed_data(self):
self.offer.amazon_feed_ref = 'incorrect json' # force fetch of feed data
self.offer.amazon_channel = False
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
return_value=common.SEARCH_LISTINGS_ITEMS_MOCK,
):
feed_info = self.offer._get_feed_data()
self.assertIn(self.offer, feed_info)
self.assertEqual(self.offer.amazon_feed_ref, '{"productType":"PRODUCT"}')
self.assertEqual(self.offer.amazon_channel, 'fbm')
def test_offer_get_feed_data_fallback_when_missing_data(self):
self.offer.amazon_feed_ref = 'incorrect json' # force fetch of feed data
with patch('odoo.addons.sale_amazon.utils.make_sp_api_request', return_value={'items': []}):
feed_info = self.offer._get_feed_data()
self.assertIn(self.offer, feed_info)
self.assertEqual(self.offer.amazon_feed_ref, '{"productType":"PRODUCT"}')
self.assertEqual(self.offer.amazon_channel, 'fba')
@mute_logger('odoo.addons.sale_amazon.models.amazon_offer')
def test_offer_get_feed_data_fails_gracefully(self):
self.offer.amazon_feed_ref = 'incorrect json' # force fetch of feed data
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
side_effect=amazon_utils.AmazonRateLimitError(''),
):
feed_info = self.offer._get_feed_data()
self.assertNotIn(self.offer, feed_info)
self.assertEqual(self.offer.amazon_sync_status, 'error')
def test_offer_get_feed_info_calls_sp_api_only_if_needed(self):
# Every necessary feed data is already stored
self.offer.amazon_feed_ref = '{"productType":"PRODUCT"}'
self.offer.amazon_channel = 'fba'
with patch('odoo.addons.sale_amazon.utils.make_sp_api_request') as make_sp_api_request_mock:
feed_info = self.offer._get_feed_data()
make_sp_api_request_mock.assert_not_called() # does not need to be called anymore
self.assertIn(self.offer, feed_info)
def test_schedule_inventory_reset_if_order_channel_is_fba_but_offer_is_fbm(self):
self.offer.sku = 'TEST'
self.offer.amazon_channel = 'fbm'
self.account.last_orders_sync = datetime(1, 1, 1) # Incomming order should be newer ^^
operation_responses_map = {
**common.OPERATIONS_RESPONSES_MAP,
'searchOrders': {
'lastUpdatedBefore': '2020-01-01T00:00:00Z',
'orders': [{
**common.GET_ORDER_MOCK,
"fulfillment": {
"fulfillmentStatus": "SHIPPED",
"fulfilledBy": "AMAZON",
"fulfillmentServiceLevel": "SHIPPING-CODE",
},
}],
},
}
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda _account, operation_, **_kwargs: operation_responses_map[operation_],
):
self.account._sync_orders(auto_commit=False)
self.assertEqual(self.offer.amazon_sync_status, 'reset')
self.assertFalse(self.offer.amazon_channel)
def test_no_inventory_reset_if_order_channel_is_fbm_but_offer_is_fba(self):
self.offer.sku = 'TEST'
self.offer.amazon_channel = 'fba'
old_sync_status = self.offer.amazon_sync_status
self.account.last_orders_sync = datetime(1, 1, 1) # Incomming order should be newer ^^
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda _account, operation_, **_kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
):
self.account._sync_orders(auto_commit=False)
self.assertEqual(self.offer.amazon_sync_status, old_sync_status) # No inventory reset
self.assertFalse(self.offer.amazon_channel) # Will be re-updated with next call to _sync_inventory
def test_inventory_reset(self):
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 100)
self.offer.amazon_channel = False
self.offer.amazon_sync_status = 'reset'
def submit_feed_mock_(_account, feed_, *_args, **_kwargs):
decoded_feed_ = json.loads(feed_)
message_ = decoded_feed_['messages'][0]
self.assertEqual(message_['sku'], self.offer.sku)
self.assertEqual(message_["patches"][0]["value"][0]["quantity"], 0)
return 'An_amazing_id'
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
return_value={'items': [common.FBA_LISTINGS_ITEM_MOCK]},
), patch(
'odoo.addons.sale_amazon.utils.submit_feed', new=submit_feed_mock_,
):
self.account._sync_inventory()
def test_no_stock_reset_if_offer_is_fbm(self):
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 100)
self.offer.amazon_channel = False
self.offer.amazon_sync_status = 'reset'
def submit_feed_mock_(_account, feed_, *_args, **_kwargs):
decoded_feed_ = json.loads(feed_)
message_ = decoded_feed_['messages'][0]
self.assertEqual(message_['sku'], self.offer.sku)
self.assertEqual(message_["patches"][0]["value"][0]['quantity'], 100)
return 'An_amazing_id'
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
return_value={'items': [common.FBM_LISTINGS_ITEM_MOCK]},
), patch(
'odoo.addons.sale_amazon.utils.submit_feed', new=submit_feed_mock_,
):
self.account._sync_inventory()
@mute_logger('odoo.addons.sale_amazon.models.amazon_account')
@mute_logger('odoo.addons.sale_amazon.models.stock_picking')
def test_sync_pickings(self):
""" Test the pickings confirmation synchronization. """
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda account_, operation_, **_kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
), patch(
'odoo.addons.sale_amazon.utils.submit_feed', new=Mock(return_value='An_amazing_id'),
) as mock:
self.account._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('amazon_order_ref', '=', '123456789')])
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.assertEqual(len(picking), 1, msg="FBM orders should generate exactly one picking.")
picking.carrier_id, picking.carrier_tracking_ref = self.carrier, self.tracking_ref
picking._action_done()
self.assertEqual(picking.amazon_sync_status, 'pending')
self.account.company_id.write({"street": "street1"}) # needed in no-demo
picking._sync_pickings(account_ids=(self.account.id,))
self.assertEqual(
mock.call_count,
1,
msg="An order fulfillment feed should be sent to Amazon for each confirmed "
"picking.",
)
self.assertEqual(picking.amazon_sync_status, 'processing')
def test_find_matching_product_search(self):
""" Test the product search based on the internal reference. """
self.env['product.product'].create({
'name': "Test Name",
'type': 'consu',
'default_code': 'TEST_CODE',
})
self.assertTrue(self.account._find_matching_product('TEST_CODE', None, None, None))
def test_find_matching_product_use_fallback(self):
""" Test the product search failure with use of the fallback. """
default_product = self.env['product.product'].create({
'name': "Default Name", 'type': 'consu'
})
self.env['ir.model.data'].create({
'module': 'sale_amazon',
'name': 'test_xmlid',
'model': 'product.product',
'res_id': default_product.id,
})
self.assertTrue(
self.account._find_matching_product('INCORRECT_CODE', 'test_xmlid', None, None)
)
def test_find_matching_product_regen_fallback(self):
""" Test the product search failure with regeneration of the fallback. """
default_product = self.env['product.product'].create({
'name': "Default Name", 'type': 'consu',
})
self.env['ir.model.data'].create({
'module': 'sale_amazon',
'name': 'test_xmlid',
'model': 'product.product',
'res_id': default_product.id,
})
default_product.unlink() # Simulate deletion of the default product added with data
product = self.account._find_matching_product(
'INCORRECT_CODE', 'test_xmlid', 'Default Name', 'consu'
)
self.assertEqual(product.name, 'Default Name')
self.assertEqual(product.type, 'consu')
self.assertEqual(product.list_price, 0.)
self.assertFalse(product.sale_ok)
self.assertFalse(product.purchase_ok)
def test_find_matching_product_no_fallback(self):
""" Test the product search failure without regeneration of the fallback. """
self.assertFalse(self.account._find_matching_product(
'INCORRECT_CODE', 'test_xmlid', 'Default Name', 'consu', fallback=False
))
self.assertFalse(self.env.ref('sale_amazon.test_xmlid', raise_if_not_found=False))
def test_get_pricelist_search(self):
""" Test the pricelist search. """
currency = self.env['res.currency'].create({'name': 'TEST', 'symbol': 'T'})
self.env['product.pricelist'].create({
'name': 'Amazon Pricelist %s' % currency.name,
'active': False,
'currency_id': currency.id,
})
pricelist = self.env['product.pricelist']
pricelists_count = pricelist.with_context(active_test=False).search_count([])
self.assertTrue(self.account._find_or_create_pricelist(currency))
self.assertEqual(self.env['product.pricelist'].with_context(
active_test=False).search_count([]), pricelists_count)
def test_get_pricelist_creation(self):
""" Test the pricelist creation. """
currency = self.env['res.currency'].create({'name': 'TEST', 'symbol': 'T'})
pricelist = self.env['product.pricelist']
pricelists_count = pricelist.with_context(active_test=False).search_count([])
pricelist = self.account._find_or_create_pricelist(currency)
self.assertEqual(self.env['product.pricelist'].with_context(
active_test=False).search_count([]), pricelists_count + 1)
self.assertFalse(pricelist.active)
self.assertEqual(pricelist.currency_id.id, currency.id)
def test_get_partners_no_creation_same_partners(self):
""" Test the partners search with contact as delivery. """
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda account_, operation_, **kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
):
country_id = self.env['res.country'].search([('code', '=', 'US')], limit=1).id
self.env['res.partner'].create({
'name': 'Gederic Frilson',
'is_company': True,
'street': '123 RainBowMan Street',
'zip': '12345-1234',
'city': 'New Duck City DC',
'country_id': country_id,
'state_id': self.env['res.country.state'].search(
[('country_id', '=', country_id), ('code', '=', 'CA')], limit=1
).id,
'phone': '+1 234-567-8910 ext. 12345',
'customer_rank': 1,
'company_id': self.account.company_id.id,
'email': '[email protected]',
})
contacts_count = self.env['res.partner'].search_count([])
order_data = common.OPERATIONS_RESPONSES_MAP['searchOrders']['orders'][0]
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(self.env['res.partner'].search_count([]), contacts_count)
self.assertEqual(contact.id, delivery.id)
self.assertEqual(contact.type, 'contact')
self.assertEqual(contact.email, '[email protected]')
def test_get_partners_no_creation_different_partners(self):
""" Test the partners search with different partners for contact and delivery. """
country_id = self.env['res.country'].search([('code', '=', 'US')], limit=1).id
new_partner_vals = {
'is_company': True,
'street': '123 RainBowMan Street',
'zip': '12345-1234',
'city': 'New Duck City DC',
'country_id': country_id,
'state_id': self.env['res.country.state'].search(
[('country_id', '=', country_id), ('code', '=', 'CA')], limit=1
).id,
'phone': '+1 234-567-8910 ext. 12345',
'customer_rank': 1,
'company_id': self.account.company_id.id,
'email': '[email protected]',
}
contact = self.env['res.partner'].create(dict(new_partner_vals, name='Gederic Frilson'))
self.env['res.partner'].create(dict(
new_partner_vals,
name='Gederic Frilson Delivery',
type='delivery',
parent_id=contact.id,
))
partners_count = self.env['res.partner'].search_count([])
order_data = {
**common.GET_ORDER_MOCK,
"recipient": {
"deliveryAddress": {
**common.GET_ORDER_MOCK["recipient"]["deliveryAddress"],
"name": "Gederic Frilson Delivery",
}
},
}
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(self.env['res.partner'].search_count([]), partners_count)
self.assertNotEqual(contact.id, delivery.id)
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
self.assertEqual(contact.email, delivery.email)
def test_get_partners_creation_delivery(self):
""" Test the partners search with creation of the delivery. """
def get_sp_api_response_mock(_account, operation_, **_kwargs):
""" Return a mocked response without making an actual call to the SP-API. """
base_response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
if operation_ == 'searchOrders':
order_mock = base_response_["orders"][0]
return {
**base_response_,
"orders": [
{
**order_mock,
"recipient": {
"deliveryAddress": {
"addressLine1": "123 RainBowMan Street",
"phone": "+1 234-567-8910 ext. 12345",
"postalCode": "12345-1234",
"city": "New Duck City DC",
"stateOrRegion": "CA",
"countryCode": "US",
"name": "Gederic Frilson Delivery",
"addressType": "Commercial",
}
},
}
],
}
return base_response_
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request', new=get_sp_api_response_mock
):
self.env['res.partner'].create({
'name': 'Gederic Frilson',
'company_id': self.account.company_id.id,
'email': '[email protected]',
})
partners_count = self.env['res.partner'].search_count([])
order_data = common.OPERATIONS_RESPONSES_MAP['searchOrders']['orders'][0]
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 1,
msg="A delivery partner should be created when a field of the address is different "
"from that of the contact.",
)
self.assertNotEqual(contact.id, delivery.id)
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
self.assertEqual(delivery.company_id.id, self.account.company_id.id)
self.assertEqual(contact.email, delivery.email)
def test_get_partners_creation_contact(self):
""" Test the partners search with creation of the contact. """
with patch(
'odoo.addons.sale_amazon.utils.make_sp_api_request',
new=lambda account_, operation_, **kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
):
partners_count = self.env['res.partner'].search_count([])
order_data = common.OPERATIONS_RESPONSES_MAP['searchOrders']['orders'][0]
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 1,
"No delivery partner should be created when the contact is not found and the name "
"on the order is the same as that of the address.",
)
self.assertEqual(contact.id, delivery.id)
self.assertEqual(contact.name, 'Gederic Frilson')
self.assertEqual(contact.type, 'contact')
self.assertTrue(contact.is_company)
self.assertEqual(contact.street, '123 RainBowMan Street')
self.assertFalse(contact.street2)
self.assertEqual(contact.zip, '12345-1234')
self.assertEqual(contact.city, 'New Duck City DC')
self.assertEqual(contact.country_id.code, 'US')
self.assertEqual(contact.state_id.code, 'CA')
self.assertEqual(contact.phone, '+1 234-567-8910 ext. 12345')
self.assertEqual(contact.customer_rank, 1)
self.assertEqual(contact.company_id.id, self.account.company_id.id)
self.assertEqual(contact.email, '[email protected]')
def test_get_partners_creation_contact_delivery(self):
""" Test the partners search with creation of the contact and delivery. """
partners_count = self.env['res.partner'].search_count([])
order_data = {
**common.GET_ORDER_MOCK,
"buyer": {**common.GET_ORDER_MOCK["buyer"], "buyerName": "Not Gederic Frilson"},
}
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 2,
msg="A contact partner and a delivery partner should be created when the contact "
"is not found and the name on the order is different from that of the address.",
)
self.assertNotEqual(contact.id, delivery.id)
self.assertEqual(contact.type, 'contact')
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
self.assertEqual(contact.company_id.id, delivery.company_id.id)
self.assertEqual(contact.email, delivery.email)
def test_get_partners_missing_buyer_name(self):
""" Test the partners search with missing buyer name. """
self.env['res.partner'].create({
'name': 'Gederic Frilson',
'company_id': self.account.company_id.id,
'email': '[email protected]',
})
partners_count = self.env['res.partner'].search_count([])
order_data = {
**common.GET_ORDER_MOCK,
"buyer": {**common.GET_ORDER_MOCK["buyer"], "buyerName": None},
}
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 2,
msg="A contact partner should be created when the buyer name is missing, "
"regardless of whether the same customer already had a partner, and a delivery "
"partner should also be created if the address name is different.",
)
self.assertNotEqual(contact.id, delivery.id)
self.assertEqual(contact.type, 'contact')
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
self.assertEqual(contact.email, '[email protected]')
self.assertEqual(
contact.street,
'123 RainBowMan Street',
msg="Partners synchronized with partial personal information should still hold all "
"the available personal information.",
)
def test_get_partners_missing_email(self):
""" Test the partners search with missing amazon email. """
self.env['res.partner'].create({
'name': 'Gederic Frilson',
'company_id': self.account.company_id.id,
'email': '[email protected]',
})
partners_count = self.env['res.partner'].search_count([])
order_data = {
**common.GET_ORDER_MOCK,
"buyer": {**common.GET_ORDER_MOCK["buyer"], "buyerEmail": None},
}
contact, _delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 1,
msg="A contact partner should always be created when the amazon email is missing.",
)
self.assertFalse(contact.email)
def test_get_partners_arbitrary_fields(self):
""" Test the partners search with all PII filled but in arbitrary fields. """
order_data = {
**common.GET_ORDER_MOCK,
"recipient": {
"deliveryAddress": {
**common.GET_ORDER_MOCK["recipient"]["deliveryAddress"],
"addressLine1": None,
"addressLine2": "123 test Street",
}
},
}
contact, _delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertFalse(contact.street)
self.assertTrue(contact.street2)
self.assertTrue(contact.phone)
self.assertTrue(contact.customer_rank)
self.assertTrue(contact.email)
def _get_activity_count(self, contact):
"""" Return activity count of given the contact. """
activity_id = self.env.ref('mail.mail_activity_data_todo').id
return self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', 'res.partner'),
('res_id', '=', contact.id),
])
def test_activity_on_partners_for_no_matching_state(self):
""" Test activity created for the salesman if the state received in the Amazon data
didn't match any existing state when creating a partner.
"""
order_data = {
**common.GET_ORDER_MOCK,
"recipient": {
"deliveryAddress": {
**common.GET_ORDER_MOCK["recipient"]["deliveryAddress"],
"stateOrRegion": "dummy_state",
}
},
}
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertEqual(self._get_activity_count(contact), 1)
self.assertEqual(self._get_activity_count(delivery), 1)
def test_no_activity_on_partners_for_no_state_in_amazon_data(self):
""" Test no activity created for the salesman if there is no state received in the Amazon
data.
"""
order_data = {
**common.GET_ORDER_MOCK,
"recipient": {
"deliveryAddress": {
**common.GET_ORDER_MOCK["recipient"]["deliveryAddress"],
"stateOrRegion": None,
}
},
}
contact, delivery = self.account._find_or_create_partners_from_data(order_data)
self.assertFalse(self._get_activity_count(contact))
self.assertFalse(self._get_activity_count(delivery))
def test_get_amazon_offer_search(self):
""" Test the offer search. """
marketplace = self.env['amazon.marketplace'].search([('api_ref', '=', 'ATVPDKIKX0DER')])
self.env['amazon.offer'].create({
'account_id': self.account.id,
'marketplace_id': marketplace.id,
'product_id': self.account._find_matching_product(
None, 'default_product', None, None
).id,
'sku': 'TEST',
})
offers_count = self.env['amazon.offer'].search_count([])
self.assertTrue(self.account._find_or_create_offer('TEST', marketplace))
self.assertEqual(self.env['amazon.offer'].search_count([]), offers_count)
def test_get_amazon_offer_creation(self):
""" Test the offer creation. """
marketplace = self.env['amazon.marketplace'].search([('api_ref', '=', 'ATVPDKIKX0DER')])
offers_count = self.env['amazon.offer'].search_count([])
offer = self.account._find_or_create_offer('TEST', marketplace)
self.assertEqual(self.env['amazon.offer'].search_count([]), offers_count + 1)
self.assertEqual(offer.account_id.id, self.account.id)
self.assertEqual(offer.company_id.id, self.account.company_id.id)
self.assertEqual(offer.marketplace_id.api_ref, 'ATVPDKIKX0DER')
self.assertEqual(offer.sku, 'TEST')