831 lines
35 KiB
Python
831 lines
35 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
from odoo import Command, fields
|
|
from odoo.tests.common import freeze_time, tagged
|
|
from odoo.tools import mute_logger
|
|
|
|
from odoo.addons.sale_lazada import const
|
|
from odoo.addons.sale_lazada import utils as lazada_utils
|
|
from odoo.addons.sale_lazada.tests import common
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
@freeze_time('2020-02-01')
|
|
class TestLazada(common.TestLazadaCommon):
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_orders_full(self):
|
|
"""
|
|
Test the orders synchronization with on-the-fly creation of all required records.
|
|
|
|
An order with a product line is created.
|
|
"""
|
|
with (
|
|
patch(
|
|
"odoo.addons.sale_lazada.utils.make_lazada_api_request",
|
|
new=lambda operation, _shop, *_args, **_kwargs: common.OPERATIONS_RESPONSES_MAP[
|
|
operation
|
|
],
|
|
),
|
|
patch(
|
|
"odoo.addons.sale_lazada.models.lazada_shop.LazadaShop._compute_subtotal",
|
|
new=lambda _self, total_, *_args, **_kwargs: total_,
|
|
),
|
|
):
|
|
self.shop._sync_orders(auto_commit=False)
|
|
order = self.env['sale.order'].search([('lazada_order_ref', '=', common.ORDER_ID_MOCK)])
|
|
product_line = order.order_line.filtered(
|
|
lambda line: line.product_id.default_code == 'TEST-SKU-001'
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.shop.last_orders_sync_date,
|
|
fields.Datetime.now(),
|
|
msg="The last_orders_sync_date should be equal to current datetime after a"
|
|
" successful run",
|
|
)
|
|
self.assertEqual(len(order), 1, msg="An order should be created")
|
|
self.assertEqual(
|
|
len(order.order_line),
|
|
2,
|
|
msg="An order should have a product line and a shipping line",
|
|
)
|
|
shipping_line = order.order_line - product_line
|
|
self.assertEqual(len(shipping_line), 1)
|
|
self.assertEqual(shipping_line.price_unit, 10.0)
|
|
self.assertRecordValues(
|
|
order,
|
|
[
|
|
{
|
|
"date_order": datetime(2020, 1, 15),
|
|
"company_id": self.shop.company_id.id,
|
|
"user_id": self.shop.user_id.id,
|
|
"team_id": self.shop.team_id.id,
|
|
"lazada_fulfillment_type": "fbm",
|
|
"amount_total": 90.0,
|
|
}
|
|
],
|
|
)
|
|
self.assertRecordValues(
|
|
product_line,
|
|
[
|
|
{
|
|
"price_unit": 80.0,
|
|
"discount": 0.0,
|
|
"product_uom_qty": 1.0,
|
|
"product_id": self.product.id,
|
|
}
|
|
],
|
|
)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_orders_partial(self):
|
|
"""
|
|
Test the orders synchronization interruption with API throttling.
|
|
|
|
Two orders are available in the response. The first one is fully synchronized, the second
|
|
one is throttled.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, *_args, **_kwargs):
|
|
"""
|
|
Return a mocked response without making an actual call to the Lazada API.
|
|
Raise a LazadaRateLimitError when the second order is synchronized,
|
|
to simulate a throttling issue.
|
|
"""
|
|
if operation_ == 'GetOrders':
|
|
self.api_call_count += 1
|
|
if self.api_call_count == 2:
|
|
raise lazada_utils.LazadaRateLimitError(operation_)
|
|
return common.GET_ORDERS_RESPONSE_MOCK
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.api_call_count = 0
|
|
|
|
self.shop._sync_orders(auto_commit=False)
|
|
|
|
orders = self.env['sale.order'].search([
|
|
('lazada_order_ref', '=', str(common.ORDER_ID_MOCK))
|
|
])
|
|
self.assertEqual(len(orders), 1, msg="Only one order should be synchronized")
|
|
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.",
|
|
)
|
|
self.assertEqual(self.api_call_count, 2)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_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_lazada_api_response_mock(operation_, _shop, *_args, **_kwargs):
|
|
"""Return a mocked response or raise a LazadaRateLimitError without making an actual
|
|
call to the Lazada API."""
|
|
self.api_call_count += 1
|
|
if operation_ == 'GetOrders':
|
|
raise lazada_utils.LazadaRateLimitError(operation_)
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.api_call_count = 0
|
|
last_orders_sync_date_copy = self.shop.last_orders_sync_date
|
|
|
|
self.shop._sync_orders(auto_commit=False)
|
|
|
|
self.assertEqual(self.api_call_count, 1)
|
|
self.assertEqual(self.shop.last_orders_sync_date, last_orders_sync_date_copy)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_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 the shop is inactive.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, *_args, **_kwargs):
|
|
self.api_call_count += 1
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.api_call_count = 0
|
|
last_orders_sync_date_copy = self.shop.last_orders_sync_date
|
|
self.shop.write({'active': False})
|
|
|
|
self.env['lazada.shop']._sync_orders(auto_commit=False)
|
|
|
|
self.assertEqual(self.api_call_count, 0)
|
|
self.assertEqual(self.shop.last_orders_sync_date, last_orders_sync_date_copy)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_orders_fbl(self):
|
|
"""Test the orders synchronization with fulfillment type 'Fulfillment By Lazada'.
|
|
|
|
An order with a product line is created.
|
|
FBL order should generate a stock move for each product and use the FBL location.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, *_args, **_kwargs):
|
|
"""Return a mocked response with a confirmed order with FBL fulfillment type."""
|
|
if operation_ == 'GetOrders':
|
|
return {
|
|
**common.GET_ORDERS_RESPONSE_MOCK,
|
|
"data": {"orders": [common.build_order_mock(statuses=["confirmed"])]},
|
|
}
|
|
if operation_ == 'GetMultipleOrderItems':
|
|
return {
|
|
**common.GET_ORDER_ITEMS_RESPONSE_MOCK,
|
|
'data': [
|
|
{
|
|
'order_id': common.ORDER_ID_MOCK,
|
|
'order_number': common.ORDER_ID_MOCK,
|
|
'order_items': [
|
|
{**common.ORDER_ITEM_MOCK, 'is_fbl': 1, 'status': 'confirmed'}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.shop._sync_orders(auto_commit=False)
|
|
order = self.env['sale.order'].search([('lazada_order_ref', '=', common.ORDER_ID_MOCK)])
|
|
|
|
self.assertEqual(len(order), 1)
|
|
self.assertEqual(order.lazada_fulfillment_type, 'fbl')
|
|
self.assertEqual(
|
|
len(order.order_line),
|
|
2,
|
|
msg="FBL order should have a product line and a shipping line",
|
|
)
|
|
self.assertEqual(
|
|
order.warehouse_id,
|
|
self.shop.fbl_location_id.warehouse_id,
|
|
"FBL orders should use the FBL location's warehouse",
|
|
)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_orders_cancel(self):
|
|
"""
|
|
Test the cancellation synchronization of a Lazada order.
|
|
|
|
The order is first imported, then Lazada reports a cancellation that should cancel
|
|
the related sale order and mark the order items as canceled.
|
|
"""
|
|
|
|
def get_lazada_api_response(operation_, _shop, _params=None, **_kwargs):
|
|
if operation_ == 'GetOrders':
|
|
statuses = ['pending'] if not self.order_canceled else ['canceled']
|
|
return {
|
|
"code": "0",
|
|
"request_id": "0_sync_orders_cancel",
|
|
"data": {
|
|
"countTotal": 1,
|
|
"count": 1,
|
|
"orders": [{**common.build_order_mock(), "statuses": statuses}],
|
|
},
|
|
}
|
|
if operation_ == 'GetMultipleOrderItems':
|
|
status = 'pending' if not self.order_canceled else 'canceled'
|
|
return {
|
|
'code': '0',
|
|
'request_id': '0_sync_orders_cancel_items',
|
|
'data': [
|
|
{
|
|
'order_id': common.ORDER_ID_MOCK,
|
|
'order_number': common.ORDER_ID_MOCK,
|
|
'order_items': [{**common.ORDER_ITEM_MOCK, 'status': status}],
|
|
}
|
|
],
|
|
}
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with (
|
|
patch(
|
|
"odoo.addons.sale_lazada.utils.make_lazada_api_request", new=get_lazada_api_response
|
|
),
|
|
patch(
|
|
"odoo.addons.sale_lazada.models.lazada_shop.LazadaShop._compute_subtotal",
|
|
new=lambda _self, total_, *_args, **_kwargs: total_,
|
|
),
|
|
):
|
|
self.order_canceled = False
|
|
|
|
self.shop._sync_orders(auto_commit=False)
|
|
|
|
order = self.env["sale.order"].search([("lazada_order_ref", "=", common.ORDER_ID_MOCK)])
|
|
self.assertEqual(len(order), 1, "Order should be created before cancellation.")
|
|
self.order_canceled = True
|
|
|
|
with freeze_time('2020-03-01'):
|
|
self.shop._sync_orders(auto_commit=False)
|
|
|
|
self.assertEqual(order.state, 'cancel', "Canceled orders should be canceled in Odoo.")
|
|
self.assertTrue(
|
|
all(item.status == 'canceled' for item in order.order_line.lazada_order_item_ids),
|
|
"All Lazada order items should be marked as canceled.",
|
|
)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_inventory(self):
|
|
"""
|
|
Test the inventory synchronization to Lazada.
|
|
|
|
Verifies that product stock quantities are correctly sent to Lazada API
|
|
when synchronization is enabled.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, params={}, **_kwargs):
|
|
"""Return a mocked response for inventory updates."""
|
|
if operation_ == 'UpdateSellableQuantity':
|
|
self.payload = json.loads(params['payload'])
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.shop.synchronize_inventory = True
|
|
# Update the stock level of the product
|
|
self.env['stock.quant']._update_available_quantity(
|
|
self.product, self.shop.fbm_warehouse_id.lot_stock_id, 100
|
|
)
|
|
|
|
self.shop._sync_inventory(auto_commit=False)
|
|
|
|
item_payload = self.payload['Request']['Product']['Skus']['Sku'][0]
|
|
self.assertEqual(item_payload['SkuId'], str(common.ORDER_ITEM_MOCK['sku_id']))
|
|
self.assertEqual(item_payload['SellableQuantity'], 100)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_inventory_is_skipped_when_disabled(self):
|
|
"""Test that inventory synchronization is skipped when disabled for the shop."""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, *_args, **_kwargs):
|
|
"""Inventory update should not be called."""
|
|
self.api_call_count += 1
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.api_call_count = 0
|
|
self.shop.synchronize_inventory = False
|
|
self.shop._sync_inventory(auto_commit=False)
|
|
self.assertEqual(self.api_call_count, 0)
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_product_catalog_initialization(self):
|
|
"""
|
|
Test the product catalog synchronization from Lazada.
|
|
|
|
It will be initialized if the shop has no Lazada items.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, params_=None, *_args, **_kwargs):
|
|
"""Return a mocked response for product catalog."""
|
|
params_ = params_ or {}
|
|
if operation_ == 'GetProducts':
|
|
self.time_from = params_.get('update_after')
|
|
self.time_to = params_.get('update_before')
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
self.shop.last_product_catalog_sync_date = None
|
|
self.shop.lazada_item_ids = self.env['lazada.item']
|
|
self.assertEqual(len(self.shop.lazada_item_ids), 0)
|
|
|
|
self.shop._sync_product_catalog()
|
|
|
|
self.assertEqual(len(self.shop.lazada_item_ids), 1)
|
|
self.assertEqual(
|
|
self.shop.lazada_item_ids.lazada_item_extern_id, common.ORDER_ITEM_MOCK['sku_id']
|
|
)
|
|
self.assertEqual(self.shop.lazada_item_ids.product_id.default_code, 'TEST-SKU-001')
|
|
self.assertTrue(self.shop.lazada_item_ids.sync_lazada_inventory)
|
|
|
|
self.assertEqual(self.time_from, None)
|
|
self.assertEqual(self.time_to, None)
|
|
|
|
self.assertEqual(self.shop.last_product_catalog_sync_date, datetime(2020, 2, 1))
|
|
|
|
@mute_logger('odoo.addons.sale_lazada.models.lazada_shop')
|
|
def test_sync_product_catalog_update(self):
|
|
"""
|
|
Test the product catalog synchronization from Lazada after the initial synchronization.
|
|
|
|
Verifies that new products are added to the catalog and the sync date is updated
|
|
with proper time range parameters sent to the API.
|
|
"""
|
|
|
|
def get_lazada_api_response_mock(operation_, _shop, params={}, *_args, **_kwargs):
|
|
"""Return a mocked response of an updated product for product catalog."""
|
|
if operation_ == 'GetProducts':
|
|
self.time_from = params.get('update_after')
|
|
self.time_to = params.get('update_before')
|
|
return {
|
|
**common.GET_PRODUCTS_RESPONSE_MOCK,
|
|
'data': {
|
|
'total_products': 1,
|
|
'products': [
|
|
{
|
|
'item_id': 2222222222,
|
|
'status': 'Active',
|
|
'skus': [
|
|
{
|
|
'SellerSku': 'TEST-SKU-002',
|
|
'Status': 'active',
|
|
'SkuId': 10002,
|
|
'fblWarehouseInventories': [],
|
|
}
|
|
],
|
|
}
|
|
],
|
|
},
|
|
}
|
|
return common.OPERATIONS_RESPONSES_MAP[operation_]
|
|
|
|
with patch(
|
|
'odoo.addons.sale_lazada.utils.make_lazada_api_request',
|
|
new=get_lazada_api_response_mock,
|
|
):
|
|
# Create a storable product for the new SKU
|
|
self.env['product.product'].create({
|
|
'name': "Test Product 2",
|
|
'default_code': 'TEST-SKU-002',
|
|
'list_price': 100.0,
|
|
'is_storable': True,
|
|
'tracking': 'none',
|
|
})
|
|
# Ensure the initial sync date is still the same as before the update
|
|
self.assertEqual(self.shop.last_product_catalog_sync_date, self.initial_sync_date)
|
|
|
|
self.shop._sync_product_catalog()
|
|
|
|
self.assertEqual(len(self.shop.lazada_item_ids), 2)
|
|
item = self.shop.lazada_item_ids.filtered(lambda i: i.lazada_item_extern_id == '10002')
|
|
self.assertTrue(item)
|
|
self.assertTrue(item.sync_lazada_inventory)
|
|
|
|
self.assertEqual(self.time_from, '2020-01-01T07:00:00+07:00')
|
|
self.assertEqual(self.time_to, '2020-02-01T07:00:00+07:00')
|
|
|
|
updated_sync_date = lazada_utils.lazada_timestamp_to_datetime(
|
|
'2020-02-01T07:00:00+07:00'
|
|
)
|
|
self.assertEqual(self.shop.last_product_catalog_sync_date, updated_sync_date)
|
|
|
|
def test_find_matching_product(self):
|
|
"""
|
|
Test the product matching functionality using the internal reference.
|
|
|
|
Verifies that existing products are found by SKU and that non-existent products
|
|
return False when fallback is disabled.
|
|
"""
|
|
# Test match with existing internal reference
|
|
found_product = self.shop._find_matching_product(
|
|
'TEST-SKU-001', 'default_shipping', 'Default Shipping', 'service'
|
|
)
|
|
self.assertEqual(found_product.id, self.product.id)
|
|
|
|
# Test no match with non-existing internal reference
|
|
found_product = self.shop._find_matching_product(
|
|
'NONEXISTENT-SKU', 'default_shipping', 'Default Shipping', 'service', fallback=False
|
|
)
|
|
self.assertFalse(found_product)
|
|
|
|
def test_find_or_create_item(self):
|
|
"""
|
|
Test the functionality to find or create a Lazada item.
|
|
|
|
If the item already exists, it should be found and returned.
|
|
If the item does not exist, it should be created and returned.
|
|
"""
|
|
# Test existing item
|
|
found_item = self.shop._find_or_create_item(
|
|
'TEST-SKU-001', common.ORDER_ITEM_MOCK['sku_id'], 'fbm'
|
|
)
|
|
self.assertEqual(found_item.id, self.item.id)
|
|
self.assertTrue(
|
|
found_item.sync_lazada_inventory,
|
|
"Existing FBM item with storable product should be marked for synchronization.",
|
|
)
|
|
|
|
# Test new item creation
|
|
new_item = self.shop._find_or_create_item('NEW-SKU-001', 98765, 'fbm')
|
|
self.assertNotEqual(new_item.id, self.item.id)
|
|
self.assertFalse(
|
|
new_item.sync_lazada_inventory,
|
|
"Non-storable products should not be marked for synchronization.",
|
|
)
|
|
|
|
def test_find_or_create_partners_from_data(self):
|
|
"""
|
|
Test the creation of partners from order data.
|
|
|
|
Verifies that shipping and invoice partners are created with correct address
|
|
details, country, phone, and Lazada buyer external ID from order data.
|
|
"""
|
|
with patch(
|
|
'odoo.addons.sale_lazada.models.lazada_shop.LazadaShop._find_matching_product',
|
|
new=lambda _self, _sku, _default_xmlid, _default_name, _default_type, _fallback=True: (
|
|
self.product
|
|
),
|
|
):
|
|
order_data = common.build_order_mock()
|
|
order_data['order_items'] = [common.ORDER_ITEM_MOCK]
|
|
partner_shipping, partner_invoice = self.shop._find_or_create_partners_from_data(
|
|
order_data
|
|
)
|
|
|
|
self.assertTrue(partner_shipping)
|
|
self.assertTrue(partner_invoice)
|
|
self.assertEqual(partner_shipping.name, "Gederic Frilson")
|
|
self.assertEqual(partner_shipping.country_id.code, "VN")
|
|
self.assertEqual(partner_shipping.lazada_buyer_extern_id, '30001')
|
|
self.assertEqual(partner_shipping.street, "123 RainBowMan Street")
|
|
self.assertEqual(partner_shipping.street2, "Apartment 4B")
|
|
self.assertEqual(partner_shipping.zip, "12345")
|
|
self.assertEqual(partner_shipping.city, "New Duck City")
|
|
self.assertEqual(partner_shipping.phone, "+1 234-567-8910")
|
|
|
|
def test_compute_lazada_order_status(self):
|
|
"""Test the computation of Lazada delivery status based on order item statuses."""
|
|
order_data = common.build_order_mock()
|
|
order_data['order_items'] = [dict(common.ORDER_ITEM_MOCK)]
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
product_line = order.order_line.filtered("lazada_order_item_ids")
|
|
product_line.lazada_order_item_ids.status = "processing"
|
|
|
|
self.assertEqual(order.lazada_order_status, 'processing')
|
|
|
|
# Add another item with a different status to test the mixed statuses
|
|
self.env["lazada.order.item"].create({
|
|
"order_item_extern_id": 456,
|
|
"sale_order_line_id": product_line.id,
|
|
"stock_move_id": product_line.move_ids[0].id,
|
|
"status": "delivered",
|
|
})
|
|
|
|
self.assertEqual(order.lazada_order_status, 'manual')
|
|
|
|
def test_get_lazada_aggregated_status_single_status(self):
|
|
"""Should return the status itself when only one status is present."""
|
|
status = lazada_utils.get_lazada_aggregated_status(['processing'])
|
|
self.assertEqual(status, 'processing')
|
|
|
|
def test_get_lazada_aggregated_status_mixed_statuses(self):
|
|
"""Should return 'manual' when statuses are mixed."""
|
|
status = lazada_utils.get_lazada_aggregated_status(['processing', 'delivered'])
|
|
self.assertEqual(status, 'manual')
|
|
|
|
def test_get_lazada_aggregated_status_all_canceled(self):
|
|
"""Should return 'canceled' if all statuses are canceled."""
|
|
status = lazada_utils.get_lazada_aggregated_status(['canceled'])
|
|
self.assertEqual(status, 'canceled')
|
|
|
|
def test_should_create_fbm_order_with_pending_status(self):
|
|
"""FBM should create order when status is 'pending'."""
|
|
order_data_fbm_pending = {
|
|
'order_items': [{**common.ORDER_ITEM_MOCK, 'shipping_provider_type': 'standard'}],
|
|
'statuses': ['pending'],
|
|
}
|
|
self.assertTrue(
|
|
self.shop._should_create_order(order_data_fbm_pending, 'fbm'),
|
|
"FBM orders with 'pending' status should be synchronized.",
|
|
)
|
|
|
|
def test_should_not_create_fbm_order_with_confirmed_status(self):
|
|
"""FBM should not create order when status is 'confirmed'."""
|
|
order_data_fbm_confirmed = {
|
|
"order_id": common.ORDER_ID_MOCK,
|
|
'order_items': [{**common.ORDER_ITEM_MOCK, 'shipping_provider_type': 'standard'}],
|
|
'statuses': ['confirmed'],
|
|
}
|
|
self.assertFalse(
|
|
self.shop._should_create_order(order_data_fbm_confirmed, 'fbm'),
|
|
"FBM orders without 'pending' status should be ignored.",
|
|
)
|
|
|
|
def test_should_create_fbl_order_with_confirmed_status(self):
|
|
"""FBL should create order when status is 'confirmed'."""
|
|
order_data_fbl_confirmed = {
|
|
'order_items': [
|
|
{**common.ORDER_ITEM_MOCK, 'shipping_provider_type': 'standard', 'is_fbl': 1}
|
|
],
|
|
'statuses': ['confirmed'],
|
|
}
|
|
self.assertTrue(
|
|
self.shop._should_create_order(order_data_fbl_confirmed, 'fbl'),
|
|
"FBL orders with 'confirmed' status should be synchronized.",
|
|
)
|
|
|
|
def test_should_not_create_fbl_order_with_pending_status(self):
|
|
"""FBL should not create order when status is 'pending'."""
|
|
order_data_fbl_pending = {
|
|
"order_id": common.ORDER_ID_MOCK,
|
|
'order_items': [
|
|
{**common.ORDER_ITEM_MOCK, 'shipping_provider_type': 'standard', 'is_fbl': 1}
|
|
],
|
|
'statuses': ['pending'],
|
|
}
|
|
self.assertFalse(
|
|
self.shop._should_create_order(order_data_fbl_pending, 'fbl'),
|
|
"FBL orders without 'confirmed' status should be ignored.",
|
|
)
|
|
|
|
def test_get_fulfillment_type_fbm(self):
|
|
"""Ensure that a single FBM order item returns 'fbm' as fulfillment type."""
|
|
fbm_order_data = {
|
|
'order_id': 'TESTFBM',
|
|
'order_items': [{**common.ORDER_ITEM_MOCK, 'is_fbl': 0}],
|
|
}
|
|
self.assertEqual(self.shop._get_fulfillment_type(fbm_order_data), 'fbm')
|
|
|
|
def test_get_fulfillment_type_fbl(self):
|
|
"""Ensure that a single FBL order item returns 'fbl' as fulfillment type."""
|
|
fbl_order_data = {
|
|
'order_id': 'TESTFBL',
|
|
'order_items': [{**common.ORDER_ITEM_MOCK, 'is_fbl': 1}],
|
|
}
|
|
self.assertEqual(self.shop._get_fulfillment_type(fbl_order_data), 'fbl')
|
|
|
|
def test_get_fulfillment_type_mixed(self):
|
|
"""Ensure that mixed FBL and FBM order items return None (unsupported)."""
|
|
mixed_order_data = {
|
|
'order_id': 'TESTMIX',
|
|
'order_items': [
|
|
{**common.ORDER_ITEM_MOCK, 'is_fbl': 0},
|
|
{**common.ORDER_ITEM_MOCK, 'is_fbl': 1},
|
|
],
|
|
}
|
|
self.assertIsNone(self.shop._get_fulfillment_type(mixed_order_data))
|
|
|
|
# --- 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._compute_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._compute_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_lazada.models.lazada_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.
|
|
items = [
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90101,
|
|
"item_price": 15.0,
|
|
"paid_price": 10.0,
|
|
"currency": "USD",
|
|
},
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90102,
|
|
"item_price": 15.0,
|
|
"paid_price": 10.0,
|
|
"currency": "USD",
|
|
},
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90103,
|
|
"item_price": 15.0,
|
|
"paid_price": 10.0,
|
|
"currency": "USD",
|
|
},
|
|
]
|
|
order_data = common.build_order_mock(
|
|
order_id="ADJ_RESIDUE_001", items=items, shipping_fee=0, currency="USD"
|
|
)
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
adjustment_product = self.env.ref("sale_lazada.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, float(order_data["price"]))
|
|
|
|
# --- Test Shipping Lines --- #
|
|
|
|
@mute_logger("odoo.addons.sale_lazada.models.lazada_shop")
|
|
def test_free_shipping_omits_shipping_line(self):
|
|
"""``shipping_fee == 0`` produces no shipping line."""
|
|
order_data = common.build_order_mock(
|
|
order_id="FREE_SHIP_001",
|
|
items=[dict(common.ORDER_ITEM_MOCK, order_item_id=90301, paid_price=40.0)],
|
|
shipping_fee=0,
|
|
)
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
# Only one line for the one item — no shipping line.
|
|
self.assertEqual(len(order.order_line), 1)
|
|
|
|
@mute_logger("odoo.addons.sale_lazada.models.lazada_shop")
|
|
def test_shipping_line_with_tax(self):
|
|
"""Shipping line carries product's fiscal-position-mapped tax and reconciles."""
|
|
# Give the default shipping product a 7% price-exclude tax.
|
|
shipping_product = self.env.ref("sale_lazada.default_shipping_product")
|
|
shipping_product.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.
|
|
items = [
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90401,
|
|
"item_price": 20.00,
|
|
"paid_price": 20.00,
|
|
"currency": "USD",
|
|
}
|
|
]
|
|
order_data = common.build_order_mock(
|
|
order_id="SHIP_TAX_001", items=items, shipping_fee=10.70, currency="USD"
|
|
)
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
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.price_unit, 10.00)
|
|
self.assertIn(self.tax_price_exclude_7.id, shipping_line.tax_ids.ids)
|
|
self.assertEqual(order.amount_total, float(order_data["price"]) + 10.70)
|
|
|
|
# --- Test _prepare_order_lines_values --- #
|
|
|
|
def test_prepare_order_lines_values(self):
|
|
"""Product line uses the paid price as unit price; shipping comes from the order."""
|
|
self.product.taxes_id = self.tax_price_include_7
|
|
order_data = common.build_order_mock(shipping_fee=10, currency="USD")
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
shipping_product = self.quick_ref("sale_lazada.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, 1) # one ORDER_ITEM_MOCK
|
|
self.assertEqual(product_line.price_unit, 80.0) # paid 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):
|
|
"""The order-level voucher becomes one untaxed negative line for a tax-free order."""
|
|
self.product.taxes_id = [Command.clear()]
|
|
order_data = common.build_order_mock(shipping_fee=0, currency="USD", voucher="16.0")
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
discount_product = self.quick_ref("sale_lazada.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 order-level voucher.
|
|
# 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 voucher: 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": "Lazada 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["lazada.item"].create({
|
|
"product_id": product_2.id,
|
|
"shop_id": self.shop.id,
|
|
"lazada_item_extern_id": "20002",
|
|
"lazada_sku": "SKU2",
|
|
})
|
|
|
|
items = [
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90601,
|
|
"paid_price": 200.0,
|
|
"currency": "USD",
|
|
},
|
|
{
|
|
**common.ORDER_ITEM_MOCK,
|
|
"order_item_id": 90602,
|
|
"sku": "SKU2",
|
|
"sku_id": "20002",
|
|
"paid_price": 100.0,
|
|
"currency": "USD",
|
|
},
|
|
]
|
|
order_data = common.build_order_mock(
|
|
order_id="LZ_DIST_001", items=items, shipping_fee=0, currency="USD", voucher="9.99"
|
|
)
|
|
|
|
order = self.shop._create_order_from_data(order_data)
|
|
|
|
discount_product = self.quick_ref("sale_lazada.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)
|