import json from contextlib import contextmanager from unittest.mock import patch, DEFAULT import requests from odoo.exceptions import UserError from odoo.tests import Form, TransactionCase, tagged from odoo import Command, _ from odoo.tools import float_repr, mute_logger from ..models.sendcloud_service import SendCloud BALEARES_RANGE = {str(i) for i in range(7025, 7035)} CARRIER_CODE_BY_METHOD_ID = { 4: 'spain_express:baleares', } # as shipping-price has method_id only @contextmanager def _mock_sendcloud_call(warehouse_id, specific_sendcloud_check=None): def is_baleares_carrier_applicable(params, code=None, method_id=None, for_listing=False): """ Returns True if 'spain_express:baleares' is applicable for the given shipping params. """ carrier_code = code or CARRIER_CODE_BY_METHOD_ID.get(method_id) from_pc = params.get('from_postal_code') to_pc = params.get('to_postal_code') to_country = params.get('to_country') if carrier_code != 'spain_express:baleares': return True if not to_country: return for_listing # for listing shipping methods if to_country != 'ES': return False if from_pc and to_pc: return from_pc in BALEARES_RANGE and to_pc in BALEARES_RANGE return for_listing 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, }, }, { "name": "Spain Baleares Express", "carrier": "baleares_express", "service_points_carrier": "baleares_express", "available_functionalities": { "service_area": ["domestic"], }, "code": "spain_express:baleares", "weight_range": { "min_weight": 1, "max_weight": 10001, }, "methods": [ { "id": 4, "name": "Spain Baleares Express 0-10 kg", "shipping_product_code": "spain_express:baleares", "properties": { "min_weight": 1, "max_weight": 10001, }, } ], }, ], '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' }], 4: [{ 'price': '6.6', 'currency': 'EUR' }], }, 'shipping-functionalities': {}, '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': 3}, '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: if specific_sendcloud_check: specific_sendcloud_check(endpoint, kwargs.get('json', {})) response = requests.Response() if endpoint == 'shipping-products': filtered_products = [] for product in content: if not is_baleares_carrier_applicable(kwargs.get('params', {}), code=product.get('code'), for_listing=True): continue # do not include the product if 'weight' in kwargs.get('params', {}): weight = kwargs['params']['weight'] product['methods'] = [m for m in product.get('methods', []) if m['properties']['min_weight'] <= weight <= m['properties']['max_weight']] filtered_products.append(product) content = filtered_products elif endpoint == 'shipping-price': method_id = (kwargs.get('params') or args[3]).get('shipping_method_id') if not is_baleares_carrier_applicable(kwargs.get('params', {}), method_id=method_id): content = [{'price': None, 'currency': None, 'to_country': 'ES', 'breakdown': []}] else: 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 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', 'vat': 'BE0477472701', }) cls.warehouse_id.partner_id.email = "test@odoo.com" # deco_art will be in europe cls.eu_partner = cls.env['res.partner'].create({ 'name': 'Acme Corporation', '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': 'acme.corp82@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', 'vat': '12-3456789', }) 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.do_unreserve() # Create packages picking.move_ids[0].quantity = 2.0 wiz_action = picking.action_put_in_pack() put_in_pack = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({ '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 = 1.0 wiz_action = picking.action_put_in_pack() put_in_pack = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({ '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 & average 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(12, 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'), ('Innsbruck Straße 8/1/13', '8/1/13'), ('7-3/11A Hochköning Straße', '7-3/11A'), ('Place de l\'Alma 1.4', '1.4'), ] for address in addresses: self.assertEqual(api._get_house_number(address[0]), address[1]) def test_sendcloud_picking_batch_validation(self): """ Create 2 delivery orders with sendcloud carrier. Make them respectively valid and invalid on the carrier side. Validate the pickings in batch Since the pickings are processed unbatched on the carrier side the "UserError" of the invalid picking can not be raised and should be replaced by a warning activity. """ alien = self.env['res.users'].create({ 'login': 'Mars Man', 'name': 'Spleton', 'email': 'alien@mars.com', 'group_ids': self.env.ref('stock.group_stock_user'), }) sale_orders = self.env['sale.order'].create([ { 'partner_id': self.eu_partner.id, 'order_line': [ Command.create({ 'product_id': self.product_to_ship1.id }), ] }, { 'partner_id': self.eu_partner.id, 'order_line': [ Command.create({ 'product_id': self.product_to_ship2.id }), ] }, ]) choose_delivery_carriers = [self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({ 'carrier_id': self.sendcloud.id, 'order_id': sale_orders[index].id }) for index, wiz_action in enumerate(sale_orders.mapped(lambda so: so.action_open_delivery_wizard()))] def fail_send_shipment(pick): # side effect to throw an error for a given picking but resolve the normal call for the other def _throw_error_on_chosen_picking(*args, **kwargs): if args and args[0] == pick: raise UserError("Something went wrong, parcel not returned from Sendcloud: {'weight': ['The weight must be less than 10.001 kg']}") else: return DEFAULT return _throw_error_on_chosen_picking with _mock_sendcloud_call(self.warehouse_id): for i in range(0, len(sale_orders)): choose_delivery_carriers[i].update_price() choose_delivery_carriers[i].button_confirm() sale_orders[i].action_confirm() # check that a delivery was created for the associated carrier self.assertEqual(sale_orders[i].picking_ids.carrier_id.id, sale_orders[i].carrier_id.id) pickings = sale_orders.picking_ids pickings.action_assign() sendcloud_class = 'odoo.addons.delivery_sendcloud.models.sendcloud_service.SendCloud' with patch(sendcloud_class + '._send_shipment', side_effect=fail_send_shipment(pickings[1])): pickings.with_user(alien).button_validate() # both pickings should be validated but and activity should have been created for the invalid picking self.assertEqual(pickings.mapped('state'), ['done', 'done']) self.assertTrue(self.env['mail.activity'].search([('res_model', '=', 'stock.picking'), ('res_id', '=', pickings[1].id), ('user_id', '=', alien.id)], limit=1)) @mute_logger('odoo.tools.translate') def test_sendcloud_zonal_pricing_and_product(self): """ Test zonal shipping: - Case 1: When the company is outside Baleares and the customer is in Baleares, the carrier is rejected during price fetch due to zone mismatch with a clear UserError. - Case 2: When both company and customer are in Baleares, the correct zonal product is used and a valid shipping price is returned. Ensures invalid carriers are blocked early and valid carriers return correct zonal pricing. """ self.your_company.write({ 'country_id': self.env.ref('base.es').id, 'zip': '8013', 'city': 'Barcelona', }) # Non-Balearic Company with _mock_sendcloud_call(self.warehouse_id): api_res = self.sendcloud._get_sendcloud()._get_shipping_products('ES') user_chosen_product = self._set_product_local_cache(api_res[1]) self.sendcloud._set_sendcloud_products(user_chosen_product, False) spain_partner = self.env['res.partner'].create({ 'name': 'Spanish Customer', 'country_id': self.env.ref('base.es').id, 'zip': '7034', }) sale_order_spain = self.env['sale.order'].create({ 'partner_id': spain_partner.id, 'company_id': self.your_company.id, 'partner_shipping_id': spain_partner.id, 'order_line': [Command.create({'product_id': self.product_to_ship1.id})], }) # Case 1: Ensure Correct Shipping Method with _mock_sendcloud_call(self.warehouse_id): res_spain = self.sendcloud.sendcloud_rate_shipment(sale_order_spain) self.assertFalse(res_spain['success'], "The shipment request should fail when no shipping methods are available.") self.assertEqual( res_spain.get('error_message'), _('There is no shipping method available for this order with the selected carrier'), "It should raise a UserError if the selected carrier doesn't meet Baleares postal requirements." ) self.your_company.write({ 'zip': '7026', }) # Baleares Company # Case 2: Ensure Correct Shipping Price with _mock_sendcloud_call(self.warehouse_id): res_baleares = self.sendcloud.sendcloud_rate_shipment(sale_order_spain) self.assertEqual(res_baleares['price'], 6.6) def test_sendcloud_sends_correct_delivery_type_for_amazon(self): amazon_expected_delivery_type = self.sendcloud._get_delivery_type() self.assertEqual(amazon_expected_delivery_type, 'test') def test_picking_carrier_tracking_url(self): """ Test that the manually setting the carrier tracking reference on a picking does raise a UserError when trying to open the website URL. """ picking = self.env['stock.picking'].create({ 'partner_id': self.eu_partner.id, 'picking_type_id': self.env.ref('stock.picking_type_out').id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'carrier_id': self.sendcloud.id, 'carrier_tracking_ref': 'test tracking ref', }) with self.assertRaises(UserError): picking.open_website_url() @mute_logger('odoo.tools.translate') def test_overweight_individual_products_error_message(self): """ Test that when individual products are too heavy for the shipping method, a specific error message is raised mentioning the overweight products. """ products = self.env["product.product"].create([{ 'name': 'Super Heavy Door', 'type': 'consu', 'weight': 25.0, # exceeds the 20kg limit }, { 'name': 'Massive Window', 'type': 'consu', 'weight': 30.0, # also exceeds the limit }, { 'name': 'Light Chair', 'type': 'consu', 'weight': 5.0, # within limits }]) sale_order = self.env['sale.order'].create({ 'partner_id': self.eu_partner.id, 'order_line': [ Command.create({ 'product_id': product.id, 'product_uom_qty': 1.0, }) for product in products ] }) 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() picking = sale_order.picking_ids[0] picking.action_assign() with self.assertRaises(UserError) as cm: picking._action_done() error_message = str(cm.exception) self.assertIn("Additionally, some individual product(s) are too heavy for the heaviest available shipping method", error_message) self.assertIn("Super Heavy Door", error_message) self.assertIn("Massive Window", error_message) self.assertNotIn("Light Chair", error_message) def test_customs_information_1(self): ''' Test the content of customs information when the picking originates from a SO. ''' def customs_information_check(endpoint, payload): if endpoint == 'parcels' and payload: self.assertTrue(payload.get('parcels')) parcel = payload['parcels'][0] # Verify that only the first picking contains the freight costs self.assertFalse(parcel['parcel_items'][0]['quantity'] > 1 and parcel['customs_information']['freight_costs'] != '0.00') # Verify the customs information contains the correct VAT numbers self.assertDictEqual( parcel['customs_information']['tax_numbers'], { 'sender': [{'name': 'VAT', 'country_code': 'BE', 'value': 'BE0477472701'}], 'receiver': [{'name': 'VAT', 'country_code': 'US', 'value': '123456789'}], 'importer_of_record': [{'name': 'VAT', 'country_code': 'US', 'value': '123456789'}] } ) sale_order = self.env['sale.order'].create({ 'partner_id': self.us_partner.id, 'order_line': [ Command.create({ 'product_id': self.product_to_ship1.id, 'product_uom_qty': 3, }), ] }) 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, customs_information_check): choose_delivery_carrier.update_price() choose_delivery_carrier.button_confirm() sale_order.action_confirm() self.assertGreater(len(sale_order.picking_ids), 0) # Send only 1, leave 2 for a back order picking = sale_order.picking_ids[0] self.assertEqual(picking.carrier_id.id, sale_order.carrier_id.id) picking.move_ids[0].quantity = 1 back_order_wizard_dict = picking.button_validate() back_order_wizard = Form.from_action(self.env, back_order_wizard_dict).save() back_order_wizard.process() back_order = picking.backorder_ids back_order.action_assign() back_order.button_validate() def test_customs_information_2(self): ''' Test the content of customs information when the picking does not originate from a SO. ''' def customs_information_check(endpoint, payload): if endpoint == 'parcels' and payload: # Ensure 'product_to_ship2' is correctly declared for customs (because qty is < 1) declared_properties = next(d for d in payload['parcels'][0]['parcel_items'] if '(0.5 Units)' in d.get('description', '')) self.assertEqual( (declared_properties['weight'], declared_properties['value']), (float_repr(self.product_to_ship2.weight * 0.5, 3), self.product_to_ship2.list_price * 0.5) ) customs_information = payload['parcels'][0]['customs_information'] # Verify there are no freight charges self.assertEqual(customs_information['freight_costs'], '0.00') # Verify the invoice number has a fallback value self.assertTrue(customs_information['customs_invoice_nr']) picking = self.env['stock.picking'].create({ 'partner_id': self.us_partner.id, 'picking_type_id': self.env.ref('stock.picking_type_out').id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'carrier_id': self.sendcloud.id, 'move_ids': [ Command.create({ 'product_id': self.product_to_ship1.id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'product_uom_qty': 1, }), Command.create({ 'product_id': self.product_to_ship2.id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'product_uom_qty': 0.5, }), ], }) with _mock_sendcloud_call(self.warehouse_id, customs_information_check): picking.button_validate() def test_sendcloud_small_weight(self): """ Test that a weight smaller than 1g is does not raise an error""" self.env.ref("product.decimal_stock_weight").digits = 5 small_product = self.env["product.product"].create({ 'name': 'Small product', 'type': 'consu', 'weight': 0.00005, }) sale_order = self.env['sale.order'].create({ 'partner_id': self.eu_partner.id, 'order_line': [ Command.create({ 'product_id': small_product.id, 'product_uom_qty': 1.0, }), Command.create({ 'product_id': self.product_to_ship1.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() picking = sale_order.picking_ids[0] for move in picking.move_ids: move.quantity = move.product_uom_qty move.picked = True picking.button_validate() sender_id = api._get_pick_sender_address(picking) parcels = api._prepare_parcel(picking, sender_id, is_return=False) items = parcels[0].get('parcel_items', []) self.assertEqual(items[0]['weight'], '0.001') self.assertEqual(items[1]['weight'], '6.000') def test_get_shipping_prices_no_sendcloud_response(self): ''' Ensure no traceback when sendcloud doesn't return any price for 'shipping-price' requests ''' original_send_request = SendCloud._send_request def patched_send_request(self, endpoint, method='get', data=None, params=None, route="https://panel.sendcloud.sc/api/v2/"): if endpoint == 'shipping-price': return [] return original_send_request(self, endpoint, method, data, params, route) sale_order = self.env['sale.order'].create({ 'partner_id': self.us_partner.id, 'order_line': [ Command.create({ 'product_id': self.product_to_ship1.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 patch.object(SendCloud, '_send_request', side_effect=patched_send_request, autospec=True): with _mock_sendcloud_call(self.warehouse_id): choose_delivery_carrier.update_price() def test_sendcloud_delivery_over_max_packaging_weight(self): ''' Ensure delivery is correctly split to match the number of packages required for the order's total weight. ''' self.product_to_ship1.weight = 0.75 self.package_type.max_weight = 2 self.sendcloud.sendcloud_default_package_type_id = self.package_type 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': 7.0, }), ] }) self.assertEqual(sale_order.shipping_weight, 5.25) # SO for 5 products to deliberately "mess" package count up (7 * 0.75 = 5.25) by requesting # for 3 packages when it should be 4 because we have no way to know whether the product can # be split into multiple packages or not. Should be changed if/when indivisible units are # implemented. 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() self.assertEqual(sale_order.order_line.filtered(lambda ol: ol.is_delivery).price_total, 3.3) def test_enable_return_label(self): ''' Test format of the requests when using the "Generate Return Label" feature. ''' def return_label_format_check(endpoint, payload): if endpoint == 'parcels' and payload: parcel = payload['parcels'][0] if parcel.get('from_name'): self.assertEqual( (parcel['from_house_number'], parcel['from_address_1']), ('4557', 'De Silva St'), ) self.sendcloud.return_label_on_delivery = True self.sendcloud.sendcloud_return_id = self.sendcloud.sendcloud_shipping_id picking = self.env['stock.picking'].create({ 'partner_id': self.us_partner.id, 'picking_type_id': self.env.ref('stock.picking_type_out').id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'carrier_id': self.sendcloud.id, 'move_ids': [ Command.create({ 'product_id': self.product_to_ship1.id, 'location_id': self.warehouse_id.lot_stock_id.id, 'location_dest_id': self.eu_partner.property_stock_customer.id, 'product_uom_qty': 1, }), ], }) with _mock_sendcloud_call(self.warehouse_id, return_label_format_check): picking.button_validate() @tagged('post_install', '-at_install') class TestDeliverySendCloudPostInstall(TestDeliverySendCloud): def test_sendcloud_delivery_with_downpayment(self): """ Test validating the delivery of a SO with a downpayment. """ 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': 1.0, 'price_unit': 100, }), ] }) # Create downpayment so_context = { 'active_model': 'sale.order', 'active_ids': [sale_order.id], 'active_id': sale_order.id, } payment_params = { 'advance_payment_method': 'fixed', 'fixed_amount': 50, } downpayment = self.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params) downpayment.create_invoices() # Downpayment adds 2 SOL self.assertEqual(len(sale_order.order_line), 3) 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) picking = sale_order.picking_ids[0] picking.action_assign() picking._action_done() self.assertTrue(picking.sendcloud_parcel_ref)