forked from Mapan/odoo17e
487 lines
20 KiB
Python
487 lines
20 KiB
Python
import json
|
|
from contextlib import contextmanager
|
|
from unittest.mock import patch
|
|
import requests
|
|
|
|
from odoo.tests import TransactionCase, tagged
|
|
from odoo import Command
|
|
|
|
@contextmanager
|
|
def _mock_sendcloud_call(warehouse_id):
|
|
def _mock_request(*args, **kwargs):
|
|
method = kwargs.get('method') or args[0]
|
|
url = kwargs.get('url') or args[1]
|
|
responses = {
|
|
'get': {
|
|
'shipping-products': [
|
|
{
|
|
"name": "Test Product",
|
|
"carrier": "test",
|
|
"service_points_carrier": "test",
|
|
"available_functionalities": {
|
|
"last_mile": [
|
|
"service_point",
|
|
"home_delivery",
|
|
],
|
|
"multicollo": [
|
|
True,
|
|
],
|
|
"signature": [
|
|
True,
|
|
],
|
|
"delivery_attempts": [
|
|
1,
|
|
],
|
|
"delivery_deadline": [
|
|
"best_effort",
|
|
],
|
|
},
|
|
"methods": [
|
|
{
|
|
"id": 1,
|
|
"name": "Test 0-10 Kg",
|
|
"functionalities": {},
|
|
"shipping_product_code": "test:product",
|
|
"properties": {
|
|
"min_weight": 1,
|
|
"max_weight": 10001,
|
|
"max_dimensions": {
|
|
"length": 100,
|
|
"width": 50,
|
|
"height": 80,
|
|
"unit": "centimeter",
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Test 10-20 Kg",
|
|
"functionalities": {},
|
|
"shipping_product_code": "test:product",
|
|
"properties": {
|
|
"min_weight": 10001,
|
|
"max_weight": 20001,
|
|
"max_dimensions": {
|
|
"length": 100,
|
|
"width": 80,
|
|
"height": 50,
|
|
"unit": "centimeter",
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"id": 3,
|
|
"name": "Test 0-20 Kg with signature",
|
|
"functionalities": {
|
|
"signature": True,
|
|
},
|
|
"shipping_product_code": "test:product",
|
|
"properties": {
|
|
"min_weight": 1,
|
|
"max_weight": 20001,
|
|
"max_dimensions": {
|
|
"length": 90,
|
|
"width": 50,
|
|
"height": 50,
|
|
"unit": "centimeter",
|
|
}
|
|
},
|
|
},
|
|
],
|
|
"code": "test:product",
|
|
"weight_range": {
|
|
"min_weight": 1,
|
|
"max_weight": 20001,
|
|
},
|
|
}
|
|
],
|
|
'shipping-price': {
|
|
1: [{
|
|
'price': '1.1',
|
|
'currency': 'USD' # Usually, Sendcloud use EUR, but we here use USD to avoid currency conversion and ease the testing
|
|
}],
|
|
2: [{
|
|
'price': '2.2',
|
|
'currency': 'USD'
|
|
}],
|
|
3: [{
|
|
'price': '3.3',
|
|
'currency': 'USD'
|
|
}],
|
|
},
|
|
'addresses': {'sender_addresses': [{'contact_name': warehouse_id.name, 'id': 1}]},
|
|
'label': 'mock',
|
|
},
|
|
'post': {
|
|
'parcels': {
|
|
'parcels': [{
|
|
'tracking_number': '123',
|
|
'tracking_url': 'url',
|
|
'id': 1, 'weight': 10.0,
|
|
'shipment': {'id': 8},
|
|
'documents': [{'link': '/label', 'type': 'label'}],
|
|
'colli_uuid': '4dc5e2dc-e0be-4814-a6fd-fdf96dd4a9b6',
|
|
}],
|
|
'status': 'deleted'
|
|
},
|
|
}
|
|
}
|
|
for endpoint, content in responses[method].items():
|
|
if endpoint in url:
|
|
response = requests.Response()
|
|
if endpoint == 'shipping-products' and 'weight' in kwargs.get('params', {}):
|
|
weight = kwargs['params']['weight']
|
|
for product in content:
|
|
product['methods'] = list(filter(lambda m: m['properties']['min_weight'] <= weight <= m['properties']['max_weight'], product['methods']))
|
|
elif endpoint == 'shipping-price':
|
|
method_id = (kwargs.get('params') or args[3]).get('shipping_method_id')
|
|
content = content[method_id]
|
|
|
|
response._content = json.dumps(content).encode()
|
|
response.status_code = 200
|
|
return response
|
|
|
|
raise Exception('unhandled request url %s' % url)
|
|
|
|
with patch.object(requests.Session, 'request', _mock_request):
|
|
yield
|
|
|
|
@tagged('-standard', 'external')
|
|
class TestDeliverySendCloud(TransactionCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.your_company = cls.env.ref("base.main_partner")
|
|
cls.warehouse_id = cls.env['stock.warehouse'].search([('company_id', '=', cls.your_company.id)], limit=1)
|
|
cls.your_company.write({'name': 'Odoo SA',
|
|
'country_id': cls.env.ref('base.be').id,
|
|
'street': 'Chaussée de Namur 40',
|
|
'street2': False,
|
|
'state_id': False,
|
|
'city': 'Ramillies',
|
|
'zip': 1367,
|
|
'phone': '081813700',
|
|
})
|
|
# deco_art will be in europe
|
|
cls.eu_partner = cls.env['res.partner'].create({
|
|
'name': 'Deco Addict',
|
|
'is_company': True,
|
|
'street': '77 Santa Barbara Rd',
|
|
'city': 'Pleasant Hill',
|
|
'country_id': cls.env.ref('base.nl').id,
|
|
'zip': '1105AA',
|
|
'state_id': False,
|
|
'email': 'deco.addict82@example.com',
|
|
'phone': '(603)-996-3829',
|
|
})
|
|
# partner in us (azure)
|
|
cls.us_partner = cls.env['res.partner'].create({
|
|
'name': 'Azure Interior',
|
|
'is_company': True,
|
|
'street': '4557 De Silva St',
|
|
'city': 'Fremont',
|
|
'country_id': cls.env.ref('base.us').id,
|
|
'zip': '94538',
|
|
'state_id': cls.env.ref('base.state_us_5').id,
|
|
'email': 'azure.Interior24@example.com',
|
|
'phone': '(870)-931-0505',
|
|
})
|
|
|
|
cls.product_to_ship1 = cls.env["product.product"].create({
|
|
'name': 'Door with wings',
|
|
'type': 'consu',
|
|
'weight': 6.0,
|
|
})
|
|
|
|
cls.product_to_ship2 = cls.env["product.product"].create({
|
|
'name': 'Door with Legs',
|
|
'type': 'consu',
|
|
'weight': 12.0,
|
|
})
|
|
|
|
shipping_product = cls.env['product.product'].create({
|
|
'name': 'SendCloud Delivery',
|
|
'type': 'service',
|
|
})
|
|
|
|
cls.sendcloud = cls.env['delivery.carrier'].create({
|
|
'delivery_type': 'sendcloud',
|
|
'product_id': shipping_product.id,
|
|
'sendcloud_public_key': 'mock_key',
|
|
'sendcloud_secret_key': 'mock_key',
|
|
'name': 'SendCloud',
|
|
})
|
|
|
|
cls.package_type = cls.env['stock.package.type'].create({
|
|
'name': 'test package',
|
|
})
|
|
|
|
with _mock_sendcloud_call(cls.warehouse_id):
|
|
api_res = cls.sendcloud._get_sendcloud()._get_shipping_products('BE')
|
|
user_chosen_product = cls._set_product_local_cache(cls, api_res[0]) # only grab the first response and apply client-side transformations
|
|
cls.sendcloud._set_sendcloud_products(user_chosen_product, False)
|
|
|
|
def _set_product_local_cache(self, sc_product):
|
|
|
|
def humanize(tech_name):
|
|
return str(tech_name).replace('_', ' ').capitalize()
|
|
|
|
sc_product['local_cache'] = {
|
|
'functionalities': {
|
|
'bool_func': [],
|
|
'detail_func': {},
|
|
'customizable': {},
|
|
}
|
|
}
|
|
|
|
func_cache = sc_product['local_cache']['functionalities']
|
|
for key, values in sc_product['available_functionalities'].items():
|
|
name = humanize(key)
|
|
# Values is always of type List
|
|
if any(isinstance(v, bool) and v for v in values):
|
|
func_cache['bool_func'].append(name)
|
|
else:
|
|
for v in values:
|
|
if not (isinstance(v, bool) and not v) and (len(values) > 1 or v is not None):
|
|
humanized_value = 'None' if v is not None else humanize(v)
|
|
func_cache['detail_func'].get('name', []).append(humanized_value)
|
|
if 'name' in func_cache['detail_func']:
|
|
func_cache['detail_func']['name'] = ', '.join(func_cache['detail_func']['name'])
|
|
if len(values) > 1:
|
|
func_cache['customizable'][key] = values
|
|
|
|
return sc_product
|
|
|
|
def test_deliver_inside_eu(self):
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.eu_partner.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_to_ship1.id
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product_to_ship2.id
|
|
})
|
|
]
|
|
})
|
|
wiz_action = sale_order.action_open_delivery_wizard()
|
|
choose_delivery_carrier = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'carrier_id': self.sendcloud.id,
|
|
'order_id': sale_order.id
|
|
})
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
choose_delivery_carrier.update_price()
|
|
choose_delivery_carrier.button_confirm()
|
|
sale_order.action_confirm()
|
|
self.assertGreater(len(sale_order.picking_ids), 0, "The Sales Order did not generate pickings for shipment.")
|
|
|
|
picking = sale_order.picking_ids[0]
|
|
self.assertEqual(picking.carrier_id.id, sale_order.carrier_id.id, "Carrier is not the same on Picking and on SO.")
|
|
picking.action_assign()
|
|
self.assertGreater(picking.weight, 0.0, "Picking weight should be positive.")
|
|
|
|
picking._action_done()
|
|
self.assertIsNot(picking.sendcloud_parcel_ref, False,
|
|
"SendCloud Parcel Id not set")
|
|
|
|
def test_deliver_outside_eu(self):
|
|
'''
|
|
Same workflow as inside EU but tests other inner workings of sendcloud service
|
|
'''
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.us_partner.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_to_ship1.id
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product_to_ship2.id
|
|
})
|
|
]
|
|
})
|
|
wiz_action = sale_order.action_open_delivery_wizard()
|
|
choose_delivery_carrier = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'carrier_id': self.sendcloud.id,
|
|
'order_id': sale_order.id
|
|
})
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
choose_delivery_carrier.update_price()
|
|
choose_delivery_carrier.button_confirm()
|
|
sale_order.action_confirm()
|
|
self.assertGreater(len(sale_order.picking_ids), 0, "The Sales Order did not generate pickings for ups shipment.")
|
|
|
|
picking = sale_order.picking_ids[0]
|
|
self.assertEqual(picking.carrier_id.id, sale_order.carrier_id.id, "Carrier is not the same on Picking and on SO.")
|
|
picking.action_assign()
|
|
self.assertGreater(picking.weight, 0.0, "Picking weight should be positive.")
|
|
|
|
picking._action_done()
|
|
self.assertIsNot(picking.sendcloud_parcel_ref, False,
|
|
"SendCloud Parcel Id not set")
|
|
|
|
def test_rate_shipment(self):
|
|
""" Unit Test
|
|
In Weight : 1 pkg of id in {1,2}
|
|
Overweight : N pkg of id 3
|
|
"""
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.eu_partner.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_to_ship1.id,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product_to_ship2.id,
|
|
}),
|
|
]
|
|
})
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
res1 = self.sendcloud.with_context(order_weight=5.0).sendcloud_rate_shipment(sale_order)
|
|
res2 = self.sendcloud.with_context(order_weight=15.0).sendcloud_rate_shipment(sale_order)
|
|
res3 = self.sendcloud.with_context(order_weight=30.0).sendcloud_rate_shipment(sale_order)
|
|
|
|
self.assertAlmostEqual(res1['price'], 1.1, places=2)
|
|
self.assertAlmostEqual(res2['price'], 2.2, places=2)
|
|
self.assertAlmostEqual(res3['price'], 4.4, places=2) # 2 packages with method_id 2
|
|
self.assertIsNotNone(res3['warning_message'])
|
|
|
|
def test_user_filters(self):
|
|
"""
|
|
Unit test
|
|
"""
|
|
self.sendcloud.sendcloud_product_functionalities = {
|
|
'signature': [True],
|
|
}
|
|
api = self.sendcloud._get_sendcloud()
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
methods = api._get_shipping_methods(self.sendcloud, 'BE', 'BE')
|
|
self.assertEqual(methods[0]['id'], 3)
|
|
|
|
def test_partial_shipment(self):
|
|
""" Functional Test
|
|
SO -> Picking
|
|
Partial picking
|
|
Partial qty
|
|
"""
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.eu_partner.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_to_ship1.id,
|
|
'product_uom_qty': 2.0,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product_to_ship2.id,
|
|
'product_uom_qty': 1.0,
|
|
}),
|
|
]
|
|
})
|
|
wiz_action = sale_order.action_open_delivery_wizard()
|
|
choose_delivery_carrier = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'carrier_id': self.sendcloud.id,
|
|
'order_id': sale_order.id,
|
|
})
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
api = self.sendcloud._get_sendcloud()
|
|
choose_delivery_carrier.update_price()
|
|
choose_delivery_carrier.button_confirm()
|
|
sale_order.action_confirm()
|
|
self.assertGreater(len(sale_order.picking_ids), 0, "The Sales Order did not generate pickings for ups shipment.")
|
|
picking = sale_order.picking_ids[0]
|
|
# Assign Qty Done
|
|
picking.action_assign()
|
|
for move_line in picking.move_line_ids:
|
|
move_line.quantity = move_line.quantity / 2.0
|
|
picking.move_ids.picked = True
|
|
picking.move_ids._action_done()
|
|
self.assertGreater(picking.weight, 0.0, "Picking weight should be positive.")
|
|
# Create parcels
|
|
sender_id = api._get_pick_sender_address(picking)
|
|
parcels = api._prepare_parcel(picking, sender_id, is_return=False)
|
|
|
|
# Check qty & total weight in parcel(s)
|
|
# qty in DeliveryPackage commodities are rounded (integer)
|
|
items = parcels[0].get('parcel_items', [])
|
|
self.assertEqual(items[0].get('quantity'), 1)
|
|
self.assertEqual(items[1].get('quantity'), 1)
|
|
self.assertAlmostEqual(12, float(parcels[0].get('weight')))
|
|
|
|
def test_multicollo(self):
|
|
""" Functional Test
|
|
Single batch
|
|
"""
|
|
self.sendcloud.sendcloud_use_batch_shipping = True
|
|
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.eu_partner.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_to_ship1.id,
|
|
'product_uom_qty': 2.0,
|
|
}),
|
|
Command.create({
|
|
'product_id': self.product_to_ship2.id,
|
|
'product_uom_qty': 1.0,
|
|
}),
|
|
]
|
|
})
|
|
wiz_action = sale_order.action_open_delivery_wizard()
|
|
choose_delivery_carrier = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'carrier_id': self.sendcloud.id,
|
|
'order_id': sale_order.id,
|
|
})
|
|
with _mock_sendcloud_call(self.warehouse_id):
|
|
api = self.sendcloud._get_sendcloud()
|
|
choose_delivery_carrier.update_price()
|
|
choose_delivery_carrier.button_confirm()
|
|
sale_order.action_confirm()
|
|
self.assertGreater(len(sale_order.picking_ids), 0, "The Sales Order did not generate pickings for ups shipment.")
|
|
picking = sale_order.picking_ids[0]
|
|
picking.action_assign()
|
|
# Create packages
|
|
picking.move_ids[0].quantity_done = 2.0
|
|
wiz_action = picking.action_put_in_pack()
|
|
put_in_pack = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'delivery_package_type_id': self.package_type.id,
|
|
})
|
|
put_in_pack._compute_weight_uom_name()
|
|
put_in_pack._compute_shipping_weight()
|
|
put_in_pack.action_put_in_pack()
|
|
picking.move_ids[1].quantity_done = 1.0
|
|
wiz_action = picking.action_put_in_pack()
|
|
put_in_pack = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({
|
|
'delivery_package_type_id': self.package_type.id,
|
|
})
|
|
put_in_pack._compute_weight_uom_name()
|
|
put_in_pack._compute_shipping_weight()
|
|
put_in_pack.action_put_in_pack()
|
|
|
|
picking.move_ids._action_done()
|
|
self.assertGreater(picking.weight, 0.0, "Picking weight should be positive.")
|
|
# Create parcels
|
|
sender_id = api._get_pick_sender_address(picking)
|
|
parcels = api._prepare_parcel(picking, sender_id, is_return=False)
|
|
|
|
# Check qty & total weight in parcel(s)
|
|
# qty in DeliveryPackage commodities are rounded (integer)
|
|
self.assertEqual(len(parcels), 1)
|
|
parcel = parcels[0]
|
|
self.assertEqual(parcel.get('quantity'), 2)
|
|
self.assertEqual(parcel.get('shipment', {}).get('id'), 2)
|
|
self.assertAlmostEqual(24, float(parcel.get('weight')))
|
|
|
|
def test_extract_house_number(self):
|
|
api = self.sendcloud._get_sendcloud()
|
|
addresses = [
|
|
('Friedrichstr. 13/1', '13/1'),
|
|
('Rue du pont 11 A', '11 A'),
|
|
('Place Albert 1er 15B', '15B'),
|
|
('123-456 Main Street', '123-456'),
|
|
('456B Elm St', '456B'),
|
|
('789 C Oak Avenue', '789 C'),
|
|
('20A1 Vo Thi Sau Street, Tan Dinh Ward, District 1', '20A1')
|
|
]
|
|
for address in addresses:
|
|
self.assertEqual(api._get_house_number(address[0]), address[1])
|