Files
enterprise-addons/sale_shopee/tests/test_shopee.py
T

1138 lines
50 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from unittest.mock import patch
from freezegun import freeze_time
from odoo import Command, _
from odoo.tests.common import tagged
from odoo.tools import mute_logger
from odoo.addons.sale_shopee import utils as shopee_utils
from odoo.addons.sale_shopee import const
from odoo.addons.sale_shopee.tests import common
@tagged('post_install', '-at_install')
class TestShopee(common.TestShopeeCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.startClassPatcher(freeze_time('2020-02-01'))
def test_shop_status_banned(self):
""" Test the shop status when the shop is banned.
No synchronization should be performed when the shop is banned.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
if operation_ == 'get_shop_info':
return dict(common.OPERATIONS_RESPONSES_MAP[operation_], status='BANNED')
else:
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
shop = self.shop
self.assertEqual(shop.status, 'active')
shop._update_shop_information(force_update=True)
shop._sync_orders(auto_commit=False)
shop._sync_inventory(auto_commit=False)
self.assertEqual(shop.status, 'error')
self.assertEqual(shop.last_orders_sync_date, self.initial_sync_date)
self.assertEqual(self.item.last_inventory_sync_date, self.initial_sync_date)
# --- Sync Orders --- #
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_full(self):
""" Test the orders synchronization with on-the-fly creation of all required records.
1 order with 3 order lines is created: a product line and an adjustment line.
"""
def api_mock(_shop, operation_, *_args):
if operation_ == "get_escrow_detail":
return common.build_escrow_mock()
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch("odoo.addons.sale_shopee.utils.make_shopee_api_request", new=api_mock):
self.shop._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('shopee_order_ref', '=', common.ORDER_SN_MOCK)])
order_lines = self.env['sale.order.line'].search([('order_id', '=', order.id)])
product_line = order_lines.filtered(
lambda l: l.product_id.default_code == 'TEST_SKU'
)
self.assertEqual(
self.shop.last_orders_sync_date,
datetime.now(),
msg="The last_orders_sync_date should be equal to current datetime after a"
" successful run",
)
self.assertEqual(len(order), 1)
self.assertEqual(len(order_lines), 2) # product line + shipping line
shipping_line = order_lines - product_line
self.assertEqual(len(shipping_line), 1)
self.assertEqual(shipping_line.price_unit, 10.0)
self.assertRecordValues(
order,
[
{
"origin": _(
"Shopee Order %(order)s at mock_shopee_shop", order=common.ORDER_SN_MOCK
),
"date_order": datetime(2020, 1, 15, 1),
"company_id": self.shop.company_id.id,
"user_id": self.shop.user_id.id,
"team_id": self.shop.team_id.id,
"warehouse_id": self.shop.fbs_location_id.warehouse_id.id,
"shopee_fulfillment_type": "fbm",
"amount_total": 170.0,
}
],
)
self.assertRecordValues(
product_line,
[{"price_unit": 40, "product_uom_qty": 4.0, "product_id": self.item.product_id.id}],
)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_partial(self):
""" Test the orders synchronization interruption with API throttling.
Note: last_orders_sync_date is committed to DB after each successful period of
synchronization.
e.g. each synchronizing period is 10 days (check ShopeeShop._fetch_order_list)
After synchronizing 10 days of orders, last_orders_sync_date is updated in DB.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
""" Return a mocked response without making an actual call to the Shopee API.
Raise a ShopeeRateLimitError when the second order is synchronized, to simulate a
throttling issue.
"""
if operation_ == "get_escrow_detail":
return common.build_escrow_mock()
response_ = common.OPERATIONS_RESPONSES_MAP[operation_]
if operation_ == 'get_order_list':
response_['order_list'].append(
{'order_sn': '2404098R48UXYZ'}
)
elif operation_ == 'get_order_detail':
self.order_count += 1
if self.order_count == 2:
raise shopee_utils.ShopeeRateLimitError(operation_)
return response_
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
self.order_count = 0
self.shop._sync_orders(auto_commit=False)
self.assertEqual(
self.shop.last_orders_sync_date,
self.initial_sync_date + timedelta(days=const.ORDER_LIST_DAYS_LIMIT),
msg="The last_orders_sync_date should be equal to the LastUpdateDate of the last"
" fully synchronized period, which is 10 days.",
)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_fail(self):
""" Test the orders synchronization cancellation with API throttling.
The last order synchronization date should not be updated if the rate limit of one operation
was reached.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
""" Return a mocked response or raise a ShopeeRateLimitError without making an actual
call to the Shopee API. """
if operation_ == 'get_order_list':
raise shopee_utils.ShopeeRateLimitError(operation_)
else:
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
last_orders_sync_date_copy = self.shop.last_orders_sync_date
self.shop._sync_orders(auto_commit=False)
self.assertEqual(self.shop.last_orders_sync_date, last_orders_sync_date_copy)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_no_active_shop(self):
""" Test the orders synchronization cancellation with no active shop.
No order synchronization should be performed as there is no active shop selected for the
account.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
last_orders_sync_date_copy = self.shop.last_orders_sync_date
self.account.shop_ids.write({'status': 'inactive'})
self.account.shop_ids._sync_orders(auto_commit=False)
self.assertEqual(self.shop.last_orders_sync_date, last_orders_sync_date_copy)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_fbs(self):
""" Test the orders synchronization with fulfillment type 'Fulfillment By Shopee'
FBS order should generate a stock move for each product that is not a service and no picking
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
""" Return a mocked response without making an actual call to the Shopee API. """
if operation_ == 'get_order_detail':
return {
"order_list": [
common.build_order_mock(
fulfillment_flag="fulfilled_by_shopee", order_status="SHIPPED"
)
]
}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock()
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
self.shop._sync_orders(auto_commit=False) # no order created
order = self.env['sale.order'].search([('shopee_order_ref', '=', 'O123456789')])
products = order.order_line.mapped('product_id').filtered(lambda p: p.type != 'service')
moves = self.env['stock.move'].search([('product_id', 'in', products.ids)])
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.assertEqual(order.shopee_fulfillment_type, 'fbs')
self.assertEqual(len(moves), len(products))
self.assertEqual(len(picking), 0)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_cancel(self):
""" Test the orders synchronization to cancel a created order from Shopee.
Status of the order should be updated to 'cancel' when the order is cancelled on Shopee.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
"""Return a mocked response without making an actual call to the Shopee API."""
order_status_ = "CANCELLED" if self.order_created else "READY_TO_SHIP"
if operation_ == 'get_order_detail':
return {"order_list": [common.build_order_mock(order_status=order_status_)]}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock()
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
# Sync an order created on Shopee.
order_count = self.env['sale.order'].search_count([])
self.order_created = False
self.shop._sync_orders(auto_commit=False)
self.assertEqual(self.env['sale.order'].search_count([]), order_count+1)
# Rewind last_orders_sync_date to simulate the order was created before the current time
self.shop.last_orders_sync_date = datetime.fromtimestamp(1579050000) # 2020-01-15
# Sync an order cancelled from Shopee.
self.order_created = True
self.shop._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('shopee_order_ref', '=', 'O123456789')])
self.assertEqual(order.state, 'cancel')
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_orders_cancel_abort(self):
""" Test when pickings fetched the shipping label from shopee and then the related order is
cancelled on Shopee.
The picking should be cancelled when the order is cancelled on Shopee.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
"""Return a mock response without making an actual call to the Selling Partner API."""
order_status_ = "CANCELLED" if self.order_cancelled else "PROCESSED"
if operation_ == 'get_order_detail':
return {"order_list": [common.build_order_mock(order_status=order_status_)]}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock()
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
# Sync an order created on Shopee.
self.order_cancelled = False
self.shop._sync_orders(auto_commit=False)
# Rewind last_orders_sync_date to simulate the order was created before the current time
self.shop.last_orders_sync_date = datetime.fromtimestamp(1579050000) # 2020-01-15
# Check the order state and validate the picking.
order = self.env['sale.order'].search([('shopee_order_ref', '=', common.ORDER_SN_MOCK)])
self.assertNotEqual(order.state, 'shipped')
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.order_cancelled = True
self.shop._sync_orders(auto_commit=False) #DEBUG sale != cancel
self.assertEqual(order.state, 'cancel')
self.assertEqual(picking.state, 'cancel')
# --- Sync Inventory --- #
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_inventory(self):
""" Test the inventory synchronization sends the correct inventory information to Shopee
The inventory of the product on Shopee is updated as the free quantity of the product and
the last_inventory_sync_date is updated.
"""
def get_shopee_api_response_mock(
_shop, operation_, _params="", body="", _files="", method="GET"
):
""" Return a mocked response without making an actual call to the Shopee API. """
if operation_ == 'update_stock':
self.body = body
return common.OPERATIONS_RESPONSES_MAP[operation_]
else:
raise Exception()
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
self.shop.synchronize_inventory = True
self.assertEqual(self.shop.shopee_item_ids, self.item)
self.assertEqual(self.item.product_id.free_qty, 0)
self.assertEqual(self.item.last_inventory_sync_date, self.initial_sync_date)
# setup inventory level of the product to 10
warehouse_id = self.env['stock.warehouse'].search([('company_id', '=', self.shop.company_id.id)], limit=1)
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': self.item.product_id.id,
'location_id': warehouse_id.lot_stock_id.id,
'inventory_quantity': 10,
})._apply_inventory()
self.shop._sync_inventory(auto_commit=False)
self.assertEqual(self.body['item_id'], int(self.item.shopee_item_identifier))
self.assertEqual(self.body['stock_list'][0]['seller_stock'][0]['stock'], 10)
self.assertEqual(self.item.last_inventory_sync_date, datetime.now())
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_inventory_is_skipped_when_disabled(self):
""" Test skipping the inventory synchronization when the shop disabled the feature.
No inventory synchronization should be performed when the shop is disabled.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args):
""" Return a mocked response without making an actual call to the Shopee API. """
if operation_ == 'update_stock':
raise Exception()
else:
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
self.shop.synchronize_inventory = False
self.assertEqual(self.shop.shopee_item_ids, self.item)
# No error means get_shopee_api_response_mock is not called
self.shop._sync_inventory(auto_commit=False)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_inventory_error(self):
""" Test the inventory synchronization error handling.
An exception should be raised when the inventory synchronization fails.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args, **_kwargs):
""" Return a mocked response without making an actual call to the Shopee API. """
if operation_ == 'update_stock':
raise Exception()
return common.OPERATIONS_RESPONSES_MAP[operation_]
def _handle_sync_failure(_shop):
raise Exception()
with patch('odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock),\
patch('odoo.addons.sale_shopee.models.shopee_shop.ShopeeShop._handle_sync_failure',
new=_handle_sync_failure):
self.shop.synchronize_inventory = True
self.assertEqual(self.shop.shopee_item_ids, self.item)
with self.assertRaises(Exception):
self.shop._sync_inventory(auto_commit=False)
# --- Sync Pickings --- #
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_pickings(self):
""" Test the pickings synchronization.
A tracking number and shipping label should be created for the FBM order.
"""
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=lambda _shop, operation_, *_args,
**_kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
):
# Synchronize the order and generate picking
self.shop._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('shopee_order_ref', '=', 'O123456789')])
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.assertEqual(len(picking), 1)
self.assertEqual(picking.carrier_id.name, 'Fake Ship')
self.assertEqual(picking.shopee_label_status, 'not available')
self.assertEqual(len(picking.message_ids.attachment_ids), 0)
# Synchronize the picking
picking._action_done()
self.env['stock.picking']._sync_shopee_pickings(tuple(self.shop.ids), auto_commit=False)
self.assertEqual(picking.shopee_label_status, 'stored')
self.assertEqual(picking.carrier_tracking_ref, 'MY200448706479IT')
self.assertEqual(len(picking.message_ids.attachment_ids), 1)
@mute_logger('odoo.addons.sale_shopee.models.shopee_shop')
def test_sync_pickings_processing(self):
""" Test the pickings synchronization with a temporary processing status.
The tracking number and shipping label should be created for the FBM order.
"""
def get_shopee_api_response_mock(_shop, operation_, *_args, **_kwargs):
""" Return a mocked response without making an actual call to the Shopee API. """
if operation_ == 'get_shipping_document_result' and not label_processed:
return {
'result_list': [{
'order_sn': common.ORDER_SN_MOCK,
'status': 'PROCESSING',
}]
}
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=get_shopee_api_response_mock,
):
label_processed = False
self.shop._sync_orders(auto_commit=False)
order = self.env['sale.order'].search([('shopee_order_ref', '=', 'O123456789')])
picking = self.env['stock.picking'].search([('sale_id', '=', order.id)])
self.assertEqual(picking.shopee_label_status, 'not available')
self.assertEqual(len(picking.message_ids.attachment_ids), 0)
picking._action_done()
self.env['stock.picking']._sync_shopee_pickings(tuple(self.shop.ids), auto_commit=False)
self.assertEqual(picking.shopee_label_status, 'processing')
self.assertEqual(picking.carrier_tracking_ref, 'MY200448706479IT')
self.assertEqual(len(picking.message_ids.attachment_ids), 0)
label_processed = True
self.env['stock.picking']._sync_shopee_pickings(tuple(self.shop.ids), auto_commit=False)
self.assertEqual(picking.shopee_label_status, 'stored')
self.assertEqual(len(picking.message_ids.attachment_ids), 1)
# --- Find Matching Product --- #
def test_find_matching_product_search(self):
""" Test the product search based on the internal reference.
A product should be found with the internal reference.
"""
product = self.shop._find_matching_product(
common.ORDER_ITEM_MOCK['model_sku'], None, None, None
)
self.assertEqual(product.name, self.item.product_id.name)
self.assertEqual(product.default_code, common.ORDER_ITEM_MOCK['model_sku'])
def test_find_matching_product_use_fallback(self):
""" Test the product search failure with use of the fallback.
A fallback product should be returned when the product is not found.
"""
default_product = self.env['product.product'].create({
'name': "Default Name", 'type': 'consu'
})
self.env['ir.model.data'].create({
'module': 'sale_shopee',
'name': 'test_xmlid',
'model': 'product.product',
'res_id': default_product.id,
})
self.assertTrue(
self.shop._find_matching_product('UNKNOWN_CODE', 'test_xmlid', None, None)
)
def test_find_matching_product_regen_fallback(self):
""" Test the product search failure with regeneration of the fallback.
A restored fallback product should be returned when the product is not found.
"""
self.env['ir.model.data'].create({
'module': 'sale_shopee',
'name': 'test_xmlid',
'model': 'product.product',
})
product = self.shop._find_matching_product(
'INCORRECT_CODE', 'test_xmlid', 'Default Name', 'consu'
)
self.assertRecordValues(product, [{
'name': "Default Name",
'type': 'consu',
'list_price': 0,
'sale_ok': False,
'purchase_ok': False,
}])
def test_find_matching_product_no_fallback(self):
""" Test the product search failure without regeneration of the fallback.
No product should be returned when product is not found based on internal reference and
fallback is disabled.
"""
self.assertFalse(self.shop._find_matching_product(
'INCORRECT_CODE', 'test_xmlid', 'Default Name', 'consu', fallback=False
))
self.assertFalse(self.env.ref('sale_shopee.test_xmlid', raise_if_not_found=False))
def test_find_matching_product_get_default_shipping(self):
""" Test the product search and get the default shipping product
No new item should be created when the default code is updated when
an existing shopee item with the same item_identifier is found
"""
item_count = self.env['shopee.item'].search_count([])
product = self.shop._find_matching_product(
'non_existing_sku', 'shipping_product', 'Shopee Shipping', 'service'
)
self.assertEqual(self.env['shopee.item'].search_count([]), item_count)
self.assertEqual(product.name, "Shopee Shipping")
self.assertEqual(product.type, 'service')
self.assertNotEqual(product.default_code, 'non_existing_sku')
# --- Get Pricelist --- #
def test_get_pricelist_search(self):
""" Test the pricelist search.
A pricelist should be found with the created currency.
"""
currency = self.env['res.currency'].create({'name': 'TEST', 'symbol': 'T'})
self.env['product.pricelist'].create({
'name': _("Shopee Pricelist %(currency)s", currency=currency.name),
'active': False,
'currency_id': currency.id,
})
pricelists_count = self.env['product.pricelist'].with_context(
active_test=False
).search_count([])
self.assertTrue(self.shop._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.
A pricelist should be created when the given currency has no pricelist.
"""
currency = self.env['res.currency'].create({'name': 'TEST', 'symbol': 'T'})
pricelists_count = self.env['product.pricelist'].with_context(
active_test=False
).search_count([])
pricelist = self.shop._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)
# --- Get Partner --- #
def test_get_partners_no_creation_same_partners(self):
""" Test the partners search with contact as delivery.
No contact should be created.
"""
with patch(
'odoo.addons.sale_shopee.utils.make_shopee_api_request',
new=lambda _shop, operation_, **_kwargs: common.OPERATIONS_RESPONSES_MAP[operation_],
):
country_id = self.env['res.country'].search([('code', '=', 'VN')], limit=1).id
self.env['res.partner'].create({
'name': "Gederic Frilson",
'is_company': True,
'street': "123 RainBowMan Street",
'street2': "Xã Phong Thạnh Tây B",
'zip': '',
'city': "New Duck City DC",
'country_id': country_id,
'state_id': self.env['res.country.state'].search(
[('country_id', '=', country_id), ('name', '=', 'Tây Ninh')], limit=1
).id,
'phone': "9876543210",
'customer_rank': 1,
'company_id': self.shop.company_id.id,
'shopee_buyer_identifier': 444444,
})
contacts_count = self.env['res.partner'].search_count([])
contact, delivery = self.shop._find_or_create_partners_from_data(
common.build_order_mock()
)
self.assertEqual(self.env['res.partner'].search_count([]), contacts_count)
self.assertRecordValues(contact, [{
'id': delivery.id,
'type': 'contact',
'phone': "9876543210",
}])
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', '=', 'VN')], limit=1).id
new_partner_vals = {
'is_company': True,
'street': "123 RainBowMan Street",
'street2': "Xã Phong Thạnh Tây B",
'zip': '',
'city': "New Duck City DC",
'country_id': country_id,
'state_id': self.env['res.country.state'].search(
[('country_id', '=', country_id), ('name', '=', 'Tây Ninh')], limit=1
).id,
'phone': "9876543210",
'customer_rank': 1,
'company_id': self.shop.company_id.id,
'shopee_buyer_identifier': 444444,
}
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.build_order_mock(
recipient_address=dict(common.BUYER_ADDRESS_MOCK, name="Gederic Frilson Delivery")
)
contact, delivery = self.shop._find_or_create_partners_from_data(order_data)
self.assertEqual(self.env['res.partner'].search_count([]), partners_count)
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
self.assertEqual(contact.phone, delivery.phone)
self.assertNotEqual(contact.id, delivery.id)
def test_get_partners_creation_delivery(self):
""" Test the partners search with creation of the delivery.
A delivery partner should be created when a field of the address is not strictly the
same as that of the contact.
"""
self.env['res.partner'].create({
'name': "Gederic Frilson",
'company_id': self.shop.company_id.id,
'shopee_buyer_identifier': 444444,
})
partners_count = self.env['res.partner'].search_count([])
contact, delivery = self.shop._find_or_create_partners_from_data(common.build_order_mock())
self.assertNotEqual(contact.id, delivery.id)
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 1,
)
self.assertRecordValues(delivery, [{
'type': 'delivery',
'parent_id': contact.id,
'company_id': self.shop.company_id.id,
}])
def test_get_partners_creation_contact(self):
""" Test the partners search with creation of the contact.
A contact partner should be created when the contact is not found.
"""
partners_count = self.env['res.partner'].search_count([])
contact, delivery = self.shop._find_or_create_partners_from_data(common.build_order_mock())
self.assertEqual(
self.env['res.partner'].search_count([]),
partners_count + 1,
)
country_id = self.env['res.country'].search([('code', '=', 'VN')], limit=1).id
state_id = self.env['res.country.state'].search([
('country_id', '=', country_id),
('code', '=', 'VN-37'),
], limit=1).id
self.assertRecordValues(contact, [{
'id': delivery.id,
'name': "Gederic Frilson",
'type': 'contact',
'street': "123 RainBowMan Street",
'street2': "Xã Phong Thạnh Tây B",
'zip': '',
'city': "New Duck City DC",
'country_id': country_id,
'state_id': state_id,
'phone': "9876543210",
'customer_rank': 1,
'company_id': self.shop.company_id.id,
}])
def test_get_partners_creation_contact_delivery(self):
""" Test the partners search with creation of the contact and delivery.
A contact partner and a delivery partner should be created when the delivery receipt name
is not the same as the buyer name.
"""
partners_count = self.env['res.partner'].search_count([])
order_data = common.build_order_mock(
recipient_address=dict(common.BUYER_ADDRESS_MOCK, name="Gederic Frilson Delivery")
)
contact, delivery = self.shop._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.assertRecordValues(
contact,
[{'type': 'contact', 'company_id': self.shop.company_id.id, 'phone': delivery.phone}],
)
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
def test_get_partners_missing_buyer_name(self):
""" Test the partners search with missing buyer name.
A contact partner should be created when the buyer name is different from the
contact, even they have the same shopee buyer identifier. The delivery partner
should be created if the buyer name is different from the shipping address name.
"""
self.env['res.partner'].create({
'name': "Gederic Frilson",
'company_id': self.shop.company_id.id,
'shopee_buyer_identifier': 444444,
})
partners_count = self.env['res.partner'].search_count([])
order_data = common.build_order_mock(buyer_username=None)
contact, delivery = self.shop._find_or_create_partners_from_data(order_data)
self.assertEqual(self.env['res.partner'].search_count([]), partners_count + 2)
self.assertNotEqual(contact.id, delivery.id)
self.assertRecordValues(contact, [{
'type': 'contact',
'shopee_buyer_identifier': 444444,
'street': '123 RainBowMan Street',
}])
self.assertEqual(delivery.type, 'delivery')
self.assertEqual(delivery.parent_id.id, contact.id)
# --- Find or Create Shopee Item --- #
def test_find_or_create_item_exist(self):
""" Test the item search of _find_or_create_item()
An existing item will be found without creating a new one.
"""
item_count = self.env['shopee.item'].search_count([])
item = self.shop._find_or_create_item('', common.ORDER_ITEM_MOCK['item_id'], '')
self.assertEqual(self.env['shopee.item'].search_count([]), item_count)
self.assertEqual(item.id, self.item.id)
self.assertEqual(item.sync_to_shopee, True)
def test_find_or_create_item_new_item_product(self):
""" Test the item creation and product restoration of _find_or_create_item()
A new item will be created and the default product will be linked.
"""
item_count = self.env['shopee.item'].search_count([])
item = self.shop._find_or_create_item(
'a_new_sku', 'any_item_identifier', 'any_model_identifier'
)
self.assertEqual(self.env['shopee.item'].search_count([]), item_count + 1)
self.assertEqual(item.product_id.name, 'Shopee Sale')
self.assertEqual(item.product_id.default_code, False)
self.assertEqual(item.shopee_item_identifier, 'any_item_identifier')
self.assertEqual(item.shopee_model_identifier, 'any_model_identifier')
def test_find_or_create_item_new_item(self):
""" Test the item creation with no product restoration of _find_or_create_item()
A new item will be created and linked to an existing product with the same default code.
"""
item_count = self.env['shopee.item'].search_count([])
item = self.shop._find_or_create_item(
common.ORDER_ITEM_MOCK['model_sku'], 'any_item_identifier', '', 'fbs'
)
self.assertEqual(self.env['shopee.item'].search_count([]), item_count + 1)
self.assertEqual(item.product_id.name, self.product.name)
self.assertEqual(item.product_id.default_code, common.ORDER_ITEM_MOCK['model_sku'])
self.assertRecordValues(item, [{
'shopee_item_identifier': 'any_item_identifier',
'shopee_model_identifier': '',
'sync_to_shopee': False,
}])
def test_find_or_create_item_change_item_product(self):
""" Test shopee item search and update its linked product of _find_or_create_item()
An existing item will be found and its linked product will be changed.
"""
item_count = self.env['shopee.item'].search_count([])
default_product = self.env.ref('sale_shopee.default_sale_product')
self.item.update({'product_id': default_product})
item = self.shop._find_or_create_item(
self.product.default_code,
common.ORDER_ITEM_MOCK['item_id'],
common.ORDER_ITEM_MOCK['model_id'],
'fbs',
)
self.assertEqual(
self.env['shopee.item'].search_count([]),
item_count,
"No new item should be created when the default code is changed with the same"
" item_identifier.",
)
self.assertEqual(item.product_id.name, self.product.name)
self.assertEqual(item.product_id.default_code, self.product.default_code)
msg = "The correct product should now be assigned to the item."
self.assertNotEqual(item.product_id, default_product, msg)
def test_sync_shopee_pickings_with_non_shopee_picking(self):
""" Test calling the shipment label generation on a non Shopee picking doesn't do anything.
"""
picking = self.env['stock.picking'].create({
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': self.env.ref('stock.stock_location_stock').id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
picking.action_confirm()
action_shipping_label = self.env.ref('sale_shopee.action_fetch_shipping_label')
with patch('odoo.addons.sale_shopee.utils.make_shopee_api_request') as mock_api:
result = action_shipping_label.with_context(active_model='stock.picking', active_ids=picking.id).run()
self.assertFalse(result, msg="Fetching the label of a non-shopee picking shouldn't raise.")
mock_api.assert_not_called()
# --- Computed Subtotals --- #
def test_compute_subtotal_price_include_tax(self):
"""Price-included taxes keep the line subtotal aligned with Shopee's total."""
currency = self.quick_ref("base.USD")
subtotal = self.shop._recompute_subtotal(10.0, self.tax_price_include_7, currency)
# subtotal is not rounded to compute the correct unit price
# but the rounded subtotal should be equal to the total for price-included taxes
self.assertEqual(subtotal, 10.0)
def test_compute_subtotal_price_exclude_tax(self):
"""Price-excluded taxes are backed out from Shopee's tax-included total."""
currency = self.quick_ref("base.USD")
subtotal = self.shop._recompute_subtotal(10.00, self.tax_price_exclude_7, currency)
# subtotal is not rounded to compute the correct unit price
# but the rounded subtotal should be equal to the total for price-excluded taxes
self.assertEqual(subtotal, 9.35)
# --- Test Adjustment Lines --- #
@mute_logger("odoo.addons.sale_shopee.models.shopee_shop")
def test_no_adjustment_line_for_tax_exclusive_unit_rounding(self):
"""Tax-exclusive non-divisible unit prices do not emit a rounding adjustment line."""
self.product.taxes_id = self.tax_price_exclude_7
# Keep the unrounded tax-exclusive subtotal on the line so Odoo can reconcile exactly.
item = dict(
common.ORDER_ITEM_MOCK,
model_original_price=15.0,
model_discounted_price=10.0,
model_quantity_purchased=3,
)
order_payload = common.build_order_mock(
"ADJ_RESIDUE_001", [item], buyer_paid_shipping_fee=0, currency="USD"
)
def api_mock(_shop, operation_, *_args):
if operation_ == "get_order_list":
return {
"order_list": [{"order_sn": "ADJ_RESIDUE_001"}],
"more": False,
"next_cursor": None,
}
if operation_ == "get_order_detail":
return {"order_list": [order_payload]}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(buyer_paid_shipping_fee=0)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch("odoo.addons.sale_shopee.utils.make_shopee_api_request", new=api_mock):
self.shop._sync_orders(auto_commit=False)
order = self.env["sale.order"].search(
[("shopee_order_ref", "=", "ADJ_RESIDUE_001")], limit=1
)
adjustment_product = self.env.ref("sale_shopee.default_adjustment_product")
adjustment_lines = order.order_line.filtered(
lambda line: line.product_id == adjustment_product
)
self.assertEqual(len(adjustment_lines), 0)
self.assertEqual(order.amount_total, order_payload["total_amount"])
# --- Test Shipping Lines --- #
@mute_logger("odoo.addons.sale_shopee.models.shopee_shop")
def test_free_shipping_omits_shipping_line(self):
"""A zero buyer-paid shipping fee produces no shipping line."""
order_payload = common.build_order_mock(
order_sn="FREE_SHIP_001",
items=[
dict(common.ORDER_ITEM_MOCK, model_original_price=40, model_discounted_price=40)
],
buyer_paid_shipping_fee=0,
)
def api_mock(_shop, operation_, *_args):
if operation_ == "get_order_list":
return {
"order_list": [{"order_sn": "FREE_SHIP_001"}],
"more": False,
"next_cursor": None,
}
if operation_ == "get_order_detail":
return {"order_list": [order_payload]}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(buyer_paid_shipping_fee=0)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch("odoo.addons.sale_shopee.utils.make_shopee_api_request", new=api_mock):
self.shop._sync_orders(auto_commit=False)
order = self.env["sale.order"].search(
[("shopee_order_ref", "=", "FREE_SHIP_001")], limit=1
)
# Only one line for the one item — no shipping line.
self.assertEqual(len(order.order_line), 1)
@mute_logger("odoo.addons.sale_shopee.models.shopee_shop")
def test_shipping_line_with_tax(self):
"""Shipping line carries product's fiscal-position-mapped tax and reconciles."""
# Create a shipping product carrying 7% price-exclude tax.
shipping_product = self.env["product.product"].create({
"name": "Shopee Shipping (test)",
"type": "service",
"default_code": "Fake Ship",
"list_price": 0.0,
"taxes_id": self.tax_price_exclude_7,
})
self.product.taxes_id = self.tax_price_exclude_7
# Use shipping fee=10.70 tax-incl; reverse: 10.00 tax-excl. Clean reconciliation.
item = dict(
common.ORDER_ITEM_MOCK,
model_original_price=20.00,
model_discounted_price=20.00,
model_quantity_purchased=1,
)
order_payload = common.build_order_mock(
"SHIP_TAX_001", [item], buyer_paid_shipping_fee=10.70, currency="USD"
)
def api_mock(_shop, operation_, *_args):
if operation_ == "get_order_list":
return {
"order_list": [{"order_sn": "SHIP_TAX_001"}],
"more": False,
"next_cursor": None,
}
if operation_ == "get_order_detail":
return {"order_list": [order_payload]}
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(buyer_paid_shipping_fee=10.70)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch("odoo.addons.sale_shopee.utils.make_shopee_api_request", new=api_mock):
self.shop._sync_orders(auto_commit=False)
order = self.env["sale.order"].search(
[("shopee_order_ref", "=", "SHIP_TAX_001")], limit=1
)
shipping_line = order.order_line.filtered(
lambda line: line.product_id.id == shipping_product.id
)
self.assertEqual(len(shipping_line), 1)
self.assertEqual(shipping_line.product_uom_qty, 1)
self.assertEqual(shipping_line.tax_ids, self.tax_price_exclude_7)
# Exact reconciliation with Shopee's total.
self.assertEqual(order.amount_total, order_payload["total_amount"])
# --- Test _prepare_order_lines_values --- #
def test_prepare_order_lines_values(self):
"""Product line uses the discounted price as unit price; shipping comes from the escrow."""
self.product.taxes_id = self.tax_price_include_7
order_data = common.build_order_mock(buyer_paid_shipping_fee=10, currency="USD")
def get_shopee_api_response_mock(_shop, operation_, *_args):
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(buyer_paid_shipping_fee=10)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
"odoo.addons.sale_shopee.utils.make_shopee_api_request",
new=get_shopee_api_response_mock,
):
order = self.shop._create_order_from_data(order_data)
shipping_product = self.quick_ref("sale_shopee.default_shipping_product")
product_line = order.order_line.filtered(lambda line: line.product_id == self.product)
shipping_line = order.order_line.filtered(lambda line: line.product_id == shipping_product)
self.assertEqual(len(order.order_line), 2)
self.assertEqual(product_line.product_uom_qty, 4) # ORDER_ITEM_MOCK quantity
self.assertEqual(product_line.price_unit, 40.0) # discounted price
self.assertEqual(product_line.tax_ids, self.tax_price_include_7)
self.assertEqual(shipping_line.price_unit, 10.0)
def test_prepare_order_lines_values_discount_untaxed(self):
"""Order-level vouchers and coins aggregate into one untaxed negative line."""
self.product.taxes_id = [Command.clear()]
order_data = common.build_order_mock(buyer_paid_shipping_fee=0, currency="USD")
def get_shopee_api_response_mock(_shop, operation_, *_args):
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(
buyer_paid_shipping_fee=0,
voucher_from_seller=10,
voucher_from_shopee=4,
coins=2,
)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
"odoo.addons.sale_shopee.utils.make_shopee_api_request",
new=get_shopee_api_response_mock,
):
order = self.shop._create_order_from_data(order_data)
discount_product = self.quick_ref("sale_shopee.default_discount_product")
discount_line = order.order_line.filtered(lambda line: line.product_id == discount_product)
# No product tax → a single untaxed discount line of the buyer-funded total (10+4+2).
# Field access on `discount_line` raises if more than one line matched.
self.assertEqual(discount_line.price_unit, -16.0)
self.assertFalse(discount_line.tax_ids)
def test_prepare_order_lines_values_discount_distributed_per_tax_group(self):
"""The order-level discount is split per tax group, pro-rata to each group's base.
Bases 200 (7% tax) and 100 (10% tax) share a 9.99 discount: the larger group is allocated
first (6.66) and the last group absorbs the exact remainder (3.33), summing to -9.99.
"""
tax_include_10 = self.env["account.tax"].create({
"name": "Shopee Test Tax 10% Included",
"amount": 10.0,
"price_include_override": "tax_included",
"tax_group_id": self.tax_price_include_7.tax_group_id.id,
})
self.product.taxes_id = self.tax_price_include_7
product_2 = self.env["product.product"].create({
"name": "Second product",
"default_code": "SKU2",
"taxes_id": tax_include_10.ids,
})
self.env["shopee.item"].create({
"product_id": product_2.id,
"shop_id": self.shop.id,
"shopee_item_identifier": 999999,
"shopee_model_identifier": 888888,
"sync_to_shopee": True,
})
items = [
dict(common.ORDER_ITEM_MOCK, model_discounted_price=200, model_quantity_purchased=1),
dict(
common.ORDER_ITEM_MOCK,
item_id=999999,
model_id=888888,
model_sku="SKU2",
model_discounted_price=100,
model_quantity_purchased=1,
),
]
order_data = common.build_order_mock(items=items, buyer_paid_shipping_fee=0, currency="USD")
def get_shopee_api_response_mock(_shop, operation_, *_args):
if operation_ == "get_escrow_detail":
return common.build_escrow_mock(buyer_paid_shipping_fee=0, voucher_from_seller=9.99)
return common.OPERATIONS_RESPONSES_MAP[operation_]
with patch(
"odoo.addons.sale_shopee.utils.make_shopee_api_request",
new=get_shopee_api_response_mock,
):
order = self.shop._create_order_from_data(order_data)
discount_product = self.quick_ref("sale_shopee.default_discount_product")
discount_lines = order.order_line.filtered(lambda line: line.product_id == discount_product)
self.assertEqual(len(discount_lines), 2) # one negative line per tax group
# Field access on each filtered line raises if more than one line carries that group's tax.
line_7 = discount_lines.filtered(lambda line: line.tax_ids == self.tax_price_include_7)
line_10 = discount_lines.filtered(lambda line: line.tax_ids == tax_include_10)
self.assertEqual(line_7.price_unit, -6.66)
self.assertEqual(line_10.price_unit, -3.33)