Files
enterprise-addons/delivery_dhl_rest/tests/test_delivery_dhl.py
T

1002 lines
62 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from datetime import timedelta
import requests
from contextlib import contextmanager
from unittest.mock import patch
from odoo import fields, Command
from odoo.tests import Form, TransactionCase, tagged
from ..models.dhl_request import DHLProvider
class TestDeliveryDHLCommon(TransactionCase):
def setUp(self):
super().setUp()
self.iPadMini = self.env['product.product'].create({
'name': 'iPad Mini',
'weight': 0.01,
})
self.large_desk = self.env['product.product'].create({
'name': 'Large Desk',
'weight': 0.01,
})
self.uom_unit = self.env.ref('uom.product_uom_unit')
self.incoterm = self.env['account.incoterms'].create({
'name': 'Delivered At Place',
'code': 'DAP',
})
self.env.company.write({
'incoterm_id': self.incoterm.id,
})
self.your_company = self.env.ref('base.main_partner')
self.your_company.write({
'street': "Rue du Laid Burniat 5",
'street2': "",
'city': "Saint-Hubert",
'zip': 6870,
'state_id': False,
'country_id': self.env.ref('base.be').id,
'phone': '+1 555-555-5555',
})
self.uk_co = self.env['res.company'].create({
'name': 'Fish n Chips Co',
'street': 'Downing street 10',
'street2': '',
'city': 'London',
'zip': 'SW1A 2AA',
'state_id': False,
'country_id': self.env.ref('base.uk').id,
'phone': '+1 555-555-5555',
})
self.agrolait = self.env['res.partner'].create({
'name': 'Agrolait',
'phone': '(603)-996-3829',
'street': "rue des Bourlottes, 9",
'street2': "",
'city': "Ramillies",
'zip': 1367,
'state_id': False,
'country_id': self.env.ref('base.be').id,
})
self.delta_pc = self.env['res.partner'].create({
'name': 'Delta PC',
'phone': '(803)-873-6126',
'street': "1515 Main Street",
'street2': "",
'city': "Columbia",
'zip': 29201,
'state_id': self.env.ref('base.state_us_41').id,
'country_id': self.env.ref('base.us').id,
})
self.stock_location = self.env.ref('stock.stock_location_stock')
self.customer_location = self.env.ref('stock.stock_location_customers')
self.delivery_carrier_dhl_be_dom = self.env.ref('delivery_dhl_rest.delivery_carrier_dhl_be_dom', raise_if_not_found=False)
if not self.delivery_carrier_dhl_be_dom:
product_dhl = self.env['product.product'].create({
"name": 'DHL BE',
"default_code": 'Delivery_014',
"type": 'service',
"categ_id": self.env.ref('delivery.product_category_deliveries').id,
"sale_ok": False,
"purchase_ok": False,
"list_price": 0.0,
"invoice_policy": 'order',
})
self.delivery_carrier_dhl_be_dom = self.env['delivery.carrier'].create({
"name": 'DHL BE',
"product_id": product_dhl.id,
"delivery_type": 'dhl_rest',
"dhl_product_code": 'N',
"dhl_api_key": 'apI8zX6vT2hQ9f',
"dhl_api_secret": 'S^2eZ$1aL#2nF^0d',
"dhl_account_number": '272699353',
"dhl_default_package_type_id": self.env.ref('delivery_dhl_rest.dhl_packaging_BOX').id,
})
# Ask for delivery in 5 days, at noon.
# This is to avoid errors where DHL return an error because it's not possible for them to deliver on time.
self.delivery_date = fields.Datetime.today() + timedelta(hours=12) + timedelta(days=5)
self.delivery_carrier_dhl_eu_intl = self.env.ref('delivery_dhl_rest.delivery_carrier_dhl_eu_intl', raise_if_not_found=False)
if not self.delivery_carrier_dhl_eu_intl:
product_dhl = self.env['product.product'].create({
"name": 'DHL EU International',
"default_code": 'Delivery_015',
"type": 'service',
"categ_id": self.env.ref('delivery.product_category_deliveries').id,
"sale_ok": False,
"purchase_ok": False,
"list_price": 0.0,
"invoice_policy": 'order',
})
self.delivery_carrier_dhl_eu_intl = self.env['delivery.carrier'].create({
"name": 'DHL EU International',
"product_id": product_dhl.id,
"delivery_type": 'dhl_rest',
"dhl_product_code": 'D',
"dhl_api_key": 'apI8zX6vT2hQ9f',
"dhl_api_secret": 'S^2eZ$1aL#2nF^0d',
"dhl_account_number": '272699353',
"dhl_default_package_type_id": self.env.ref('delivery_dhl_rest.dhl_packaging_BOX').id,
})
def wiz_put_in_pack(self, picking):
""" Helper to use the 'stock.put.in.pack' wizard
in order to call the 'action_put_in_pack' method.
"""
wiz_action = picking.action_put_in_pack()
self.assertEqual(wiz_action['res_model'], 'stock.put.in.pack', 'Wrong wizard returned')
wiz = Form.from_action(self.env, wiz_action)
wiz.package_type_id = picking.carrier_id.dhl_default_package_type_id
wiz.save().action_put_in_pack()
def dhl_basic_be_domestic_flow_with_insurance(self):
def update_shipping_rate(sale_order, carrier):
# I add free delivery cost in Sales order
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_order.id,
'default_carrier_id': carrier.id,
}))
choose_delivery_carrier = delivery_wizard.save()
choose_delivery_carrier.update_price()
choose_delivery_carrier.button_confirm()
def validate_shipment(sale_order):
picking = sale_order.picking_ids[0]
self.assertEqual(picking.carrier_id, sale_order.carrier_id, "Carrier is not the same on Picking and on SO.")
for move in picking.move_ids:
move.quantity = move.product_uom_qty
move.picked = True
self.assertGreater(picking.shipping_weight, 0.0, "Picking weight should be positive.")
picking.scheduled_date = self.delivery_date
picking._action_done()
self.assertTrue(picking.carrier_tracking_ref, "DHL did not return any tracking number")
self.assertGreater(picking.carrier_price, 0.0, "DHL carrying price is probably incorrect")
def cancel_shipment(sale_order):
picking = sale_order.picking_ids[0]
picking.cancel_shipment()
self.assertFalse(picking.carrier_tracking_ref, "Carrier Tracking code has not been properly deleted")
self.assertEqual(picking.carrier_price, 0.0, "Carrier price has not been properly deleted")
SaleOrder = self.env['sale.order']
delivery_carrier_dhl_be_dom_insured = self.delivery_carrier_dhl_be_dom.copy()
delivery_carrier_dhl_be_dom_insured.shipping_insurance = 80
sol_vals = {
'product_id': self.iPadMini.id,
'name': "[A1232] Large Cabinet",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 1.0,
}
so_vals = {
'partner_id': self.agrolait.id,
'order_line': [(0, None, sol_vals)],
'date_order': self.delivery_date,
}
sale_order = SaleOrder.create(so_vals)
insured_sale_order = SaleOrder.create(so_vals)
update_shipping_rate(sale_order, self.delivery_carrier_dhl_be_dom)
update_shipping_rate(insured_sale_order, delivery_carrier_dhl_be_dom_insured)
delivery_cost = sum(line.price_total for line in sale_order.order_line if line.is_delivery)
insured_delivery_cost = sum(line.price_total for line in insured_sale_order.order_line if line.is_delivery)
self.assertGreater(delivery_cost, 0.0, "DHL delivery cost for this SO has not been correctly estimated.")
self.assertGreater(insured_delivery_cost, 0.0, "DHL delivery cost for this SO has not been correctly estimated.")
self.assertGreater(insured_delivery_cost, delivery_cost, "Insured delivery cost should be greater than uninsured delivery cost.")
(sale_order | insured_sale_order).action_confirm()
self.assertEqual(len(sale_order.picking_ids), 1, "The Sales Order did not generate a picking.")
self.assertEqual(len(insured_sale_order.picking_ids), 1, "The Sales Order did not generate a picking.")
validate_shipment(sale_order)
validate_shipment(insured_sale_order)
self.assertGreater(insured_sale_order.picking_ids[0].carrier_price, sale_order.picking_ids[0].carrier_price, "Insured delivery cost should be greater than uninsured delivery cost.")
cancel_shipment(sale_order)
cancel_shipment(insured_sale_order)
def dhl_basic_international_flow(self):
SaleOrder = self.env['sale.order']
sol_vals = {
'product_id': self.iPadMini.id,
'name': "[A1232] Large Cabinet",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': self.iPadMini.lst_price
}
so_vals = {
'partner_id': self.delta_pc.id,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'order_line': [(0, None, sol_vals)],
'date_order': self.delivery_date,
}
sale_order = SaleOrder.create(so_vals)
# I add free delivery cost in Sales order
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_order.id,
'default_carrier_id': self.delivery_carrier_dhl_eu_intl.id
}))
choose_delivery_carrier = delivery_wizard.save()
choose_delivery_carrier.update_price()
# doesn't work with the mocked request, because the productCode isn't the same...
# self.assertGreater(sum(line.price_total for line in sale_order.order_line if line.is_delivery), 0.0, "DHL delivery cost for this SO has not been correctly estimated.")
choose_delivery_carrier.button_confirm()
sale_order.action_confirm()
self.assertEqual(len(sale_order.picking_ids), 1, "The Sales Order did not generate a picking.")
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.move_ids[0].quantity = 1.0
picking.move_ids[0].picked = True
self.assertGreater(picking.shipping_weight, 0.0, "Picking weight should be positive.")
picking.scheduled_date = self.delivery_date
picking._action_done()
self.assertIsNot(picking.carrier_tracking_ref, False, "DHL did not return any tracking number")
self.assertGreater(picking.carrier_price, 0.0, "DHL carrying price is probably incorrect")
picking.cancel_shipment()
self.assertFalse(picking.carrier_tracking_ref, "Carrier Tracking code has not been properly deleted")
self.assertEqual(picking.carrier_price, 0.0, "Carrier price has not been properly deleted")
def dhl_multipackage_international_flow(self):
SaleOrder = self.env['sale.order']
sol_1_vals = {
'product_id': self.iPadMini.id,
'name': "[A1232] Large Cabinet",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': self.iPadMini.lst_price
}
sol_2_vals = {
'product_id': self.large_desk.id,
'name': "[A1090] Large Desk",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': self.large_desk.lst_price
}
so_vals = {
'partner_id': self.delta_pc.id,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'order_line': [(0, None, sol_1_vals), (0, None, sol_2_vals)],
'date_order': self.delivery_date,
}
sale_order = SaleOrder.create(so_vals)
# I add free delivery cost in Sales order
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_order.id,
'default_carrier_id': self.delivery_carrier_dhl_eu_intl.id
}))
choose_delivery_carrier = delivery_wizard.save()
choose_delivery_carrier.update_price()
choose_delivery_carrier.button_confirm()
sale_order.action_confirm()
self.assertEqual(len(sale_order.picking_ids), 1, "The Sales Order did not generate a picking.")
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.")
move0 = picking.move_ids[0]
move0.quantity = 1.0
move0.picked = True
self.wiz_put_in_pack(picking)
move1 = picking.move_ids[1]
move1.quantity = 1.0
move1.picked = True
self.wiz_put_in_pack(picking)
self.assertEqual(len(picking.move_line_ids.mapped('result_package_id')), 2, "2 Packages should have been created at this point")
self.assertGreater(picking.shipping_weight, 0.0, "Picking weight should be positive.")
picking.scheduled_date = self.delivery_date
picking._action_done()
self.assertIsNot(picking.carrier_tracking_ref, False, "DHL did not return any tracking number")
picking.cancel_shipment()
self.assertFalse(picking.carrier_tracking_ref, "Carrier Tracking code has not been properly deleted")
self.assertEqual(picking.carrier_price, 0.0, "Carrier price has not been properly deleted")
def dhl_flow_from_delivery_order(self):
StockPicking = self.env['stock.picking']
order1_vals = {
'product_id': self.iPadMini.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id
}
do_vals = {
'partner_id': self.delta_pc.id,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'state': 'draft',
'move_ids': [(0, None, order1_vals)],
'scheduled_date': self.delivery_date,
}
delivery_order = StockPicking.create(do_vals)
self.assertEqual(delivery_order.state, 'draft', 'Shipment state should be draft.')
delivery_order.action_confirm()
self.assertEqual(delivery_order.state, 'assigned', 'Shipment state should be ready(assigned).')
delivery_order.move_ids.quantity = 1.0
delivery_order.move_ids.picked = True
delivery_order.button_validate()
self.assertEqual(delivery_order.state, 'done', 'Shipment state should be done.')
@tagged('-standard', 'external')
class TestDeliveryDHL(TestDeliveryDHLCommon):
def test_01_dhl_basic_be_domestic_flow_with_insurance(self):
super().dhl_basic_be_domestic_flow_with_insurance()
def test_02_dhl_basic_international_flow(self):
super().dhl_basic_international_flow()
def test_03_dhl_multipackage_international_flow(self):
super().dhl_multipackage_international_flow()
def test_04_dhl_flow_from_delivery_order(self):
super().dhl_flow_from_delivery_order()
@contextmanager
def _mock_request_call(specific_dhl_check=None):
RATE_MOCK_RESPONSE = {
"products": [
{
"productName": "MEDICAL EXPRESS",
"productCode": "C",
"localProductCode": "O",
"localProductCountryCode": "BE",
"networkTypeCode": "TD",
"isCustomerAgreement": True,
"weight": {
"volumetric": 19.01,
"provided": 19.5,
"unitOfMeasurement": "metric"
},
"totalPrice": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"price": 76.24
},
{
"currencyType": "PULCL",
"price": 0
},
{
"currencyType": "BASEC",
"price": 0
}
],
"totalPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"priceBreakdown": [
{
"typeCode": "STTXA",
"price": 13.23
},
{
"typeCode": "SPRQT",
"price": 58.88
}
]
}
],
"detailedPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"breakdown": [
{
"name": "MEDICAL EXPRESS",
"price": 58.88,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 10.22,
"rate": 21,
"basePrice": 48.66
}
]
},
{
"name": "FUEL SURCHARGE",
"serviceCode": "FF",
"localServiceCode": "FF",
"serviceTypeCode": "SCH",
"price": 17.36,
"isCustomerAgreement": False,
"isMarketedService": False,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 3.01,
"rate": 21,
"basePrice": 14.35
}
]
}
]
}
],
"pickupCapabilities": {
"nextBusinessDay": False,
"localCutoffDateAndTime": "2024-05-02T17:30:00",
"GMTCutoffTime": "19:00:00",
"pickupEarliest": "09:30:00",
"pickupLatest": "19:00:00",
"originServiceAreaCode": "BRU",
"originFacilityAreaCode": "ANN",
"pickupAdditionalDays": 0,
"pickupDayOfWeek": 4
},
"deliveryCapabilities": {
"deliveryTypeCode": "QDDF",
"estimatedDeliveryDateAndTime": "2024-05-03T23:59:00",
"destinationServiceAreaCode": "BRU",
"destinationFacilityAreaCode": "LG1",
"deliveryAdditionalDays": 0,
"deliveryDayOfWeek": 5,
"totalTransitDays": 1
},
"pricingDate": "2024-05-02"
},
{
"productName": "EXPRESS DOMESTIC",
"productCode": "N",
"localProductCode": "L",
"localProductCountryCode": "BE",
"networkTypeCode": "TD",
"isCustomerAgreement": False,
"weight": {
"volumetric": 19.01,
"provided": 19.5,
"unitOfMeasurement": "metric"
},
"totalPrice": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"price": 56.25
},
{
"currencyType": "PULCL",
"price": 0
},
{
"currencyType": "BASEC",
"price": 0
}
],
"totalPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"priceBreakdown": [
{
"typeCode": "STTXA",
"price": 9.76
},
{
"typeCode": "SPRQT",
"price": 43.44
}
]
}
],
"detailedPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"breakdown": [
{
"name": "EXPRESS DOMESTIC",
"price": 43.44,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 7.54,
"rate": 21,
"basePrice": 35.9
}
]
},
{
"name": "FUEL SURCHARGE",
"serviceCode": "FF",
"localServiceCode": "FF",
"serviceTypeCode": "SCH",
"price": 12.81,
"isCustomerAgreement": False,
"isMarketedService": False,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 2.22,
"rate": 21,
"basePrice": 10.59
}
]
}
]
}
],
"pickupCapabilities": {
"nextBusinessDay": False,
"localCutoffDateAndTime": "2024-05-02T17:30:00",
"GMTCutoffTime": "19:00:00",
"pickupEarliest": "09:30:00",
"pickupLatest": "19:00:00",
"originServiceAreaCode": "BRU",
"originFacilityAreaCode": "ANN",
"pickupAdditionalDays": 0,
"pickupDayOfWeek": 4
},
"deliveryCapabilities": {
"deliveryTypeCode": "QDDF",
"estimatedDeliveryDateAndTime": "2024-05-03T23:59:00",
"destinationServiceAreaCode": "BRU",
"destinationFacilityAreaCode": "LG1",
"deliveryAdditionalDays": 0,
"deliveryDayOfWeek": 5,
"totalTransitDays": 1
},
"pricingDate": "2024-05-02"
},
{
"productName": "EXPRESS WORLDWIDE",
"productCode": "D",
"localProductCode": "I",
"localProductCountryCode": "BE",
"networkTypeCode": "TD",
"isCustomerAgreement": True,
"weight": {
"volumetric": 19.01,
"provided": 19.5,
"unitOfMeasurement": "metric"
},
"totalPrice": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"price": 45
},
{
"currencyType": "PULCL",
"price": 0
},
{
"currencyType": "BASEC",
"price": 0
}
],
"totalPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"priceBreakdown": [
{
"typeCode": "STTXA",
"price": 7.81
},
{
"typeCode": "SPRQT",
"price": 45
}
]
}
],
"detailedPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"breakdown": [
{
"name": "EXPRESS EASY",
"price": 45,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 7.81,
"rate": 21,
"basePrice": 37.19
}
]
}
]
}
],
"pickupCapabilities": {
"nextBusinessDay": False,
"localCutoffDateAndTime": "2024-05-02T17:30:00",
"GMTCutoffTime": "19:00:00",
"pickupEarliest": "09:30:00",
"pickupLatest": "19:00:00",
"originServiceAreaCode": "BRU",
"originFacilityAreaCode": "ANN",
"pickupAdditionalDays": 0,
"pickupDayOfWeek": 4
},
"deliveryCapabilities": {
"deliveryTypeCode": "QDDF",
"estimatedDeliveryDateAndTime": "2024-05-03T23:59:00",
"destinationServiceAreaCode": "BRU",
"destinationFacilityAreaCode": "LG1",
"deliveryAdditionalDays": 0,
"deliveryDayOfWeek": 5,
"totalTransitDays": 1
},
"pricingDate": "2024-05-02"
}
]
}
SHIP_MOCK_RESPONSE = {
"shipmentTrackingNumber": "3436501003",
"cancelPickupUrl": "https://express.api.dhl.com/mydhlapi/test/pickups/PRG240502001481",
"trackingUrl": "https://express.api.dhl.com/mydhlapi/test/shipments/3436501003/tracking",
"dispatchConfirmationNumber": "PRG240502001481",
"packages": [
{
"referenceNumber": 1,
"trackingNumber": "JD014600004860442740",
"trackingUrl": "https://express.api.dhl.com/mydhlapi/test/shipments/3436501003/tracking?pieceTrackingNumber=JD014600004860442740"
}
],
"documents": [
{
"imageFormat": "PDF",
"content": "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",
"typeCode": "label"
},
],
}
INSURED_RATE_MOCK_RESPONSE = {
"products": [
{
"productName": "EXPRESS DOMESTIC",
"productCode": "N",
"localProductCode": "L",
"localProductCountryCode": "BE",
"networkTypeCode": "TD",
"isCustomerAgreement": False,
"weight": {
"volumetric": 19.01,
"provided": 19.5,
"unitOfMeasurement": "metric",
},
"totalPrice": [
{"currencyType": "BILLC", "priceCurrency": "EUR", "price": 96.85},
{"currencyType": "PULCL", "price": 0},
{"currencyType": "BASEC", "price": 0},
],
"totalPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"priceBreakdown": [
{"typeCode": "STTXA", "price": 14.9},
{"typeCode": "SPRQT", "price": 72.3},
],
},
],
"detailedPriceBreakdown": [
{
"currencyType": "BILLC",
"priceCurrency": "EUR",
"breakdown": [
{
"name": "EXPRESS DOMESTIC",
"price": 72.3,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 12.55,
"rate": 21,
"basePrice": 59.75,
},
],
},
{
"name": "FUEL SURCHARGE",
"serviceCode": "FF",
"localServiceCode": "FF",
"serviceTypeCode": "SCH",
"price": 13.55,
"isCustomerAgreement": False,
"isMarketedService": False,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "EU_VAT",
"price": 2.35,
"rate": 21,
"basePrice": 11.2,
},
],
},
{
"name": "SHIPMENT INSURANCE",
"serviceCode": "II",
"localServiceCode": "II",
"serviceTypeCode": "XCH",
"price": 11,
"isCustomerAgreement": False,
"isMarketedService": True,
"priceBreakdown": [
{
"priceType": "TAX",
"typeCode": "Insura",
"price": 0,
"rate": 0,
"basePrice": 11,
},
],
},
],
},
],
"pickupCapabilities": {
"nextBusinessDay": False,
"localCutoffDateAndTime": "2025-09-03T14:00:00",
"GMTCutoffTime": "16:00:00",
"pickupEarliest": "10:00:00",
"pickupLatest": "16:00:00",
"originServiceAreaCode": "BRU",
"originFacilityAreaCode": "LG1",
"pickupAdditionalDays": 0,
"pickupDayOfWeek": 3,
},
"deliveryCapabilities": {
"deliveryTypeCode": "QDDF",
"estimatedDeliveryDateAndTime": "2025-09-04T23:59:00",
"destinationServiceAreaCode": "BRU",
"destinationFacilityAreaCode": "LGG",
"deliveryAdditionalDays": 0,
"deliveryDayOfWeek": 4,
"totalTransitDays": 1,
},
"pricingDate": "2025-09-02",
},
],
}
def is_insured_request(url, json):
if 'rates' in url:
return 'II' in [service['serviceCode'] for service in json['productsAndServices'][0]['valueAddedServices']]
elif 'shipment' in url:
return 'II' in [service['serviceCode'] for service in json.get('valueAddedServices', [])]
def _mock_request(*args, **kwargs):
url = kwargs.get('url')
json_ = kwargs.get('json') # not to override the json module
responses = {
'rates': INSURED_RATE_MOCK_RESPONSE,
'shipments': SHIP_MOCK_RESPONSE,
} if is_insured_request(url, json_) else {
'rates': RATE_MOCK_RESPONSE,
'shipments': SHIP_MOCK_RESPONSE,
}
for endpoint, content in responses.items():
if endpoint in url:
if specific_dhl_check:
specific_dhl_check(endpoint, kwargs['json'])
response = requests.Response()
response._content = json.dumps(content).encode()
response.status_code = 200
return response
with patch.object(requests.Session, 'request', _mock_request):
yield
class TestMockedDeliveryDHL(TestDeliveryDHLCommon):
def test_01_dhl_basic_be_domestic_flow_with_insurance(self):
with _mock_request_call():
super().dhl_basic_be_domestic_flow_with_insurance()
def test_02_dhl_basic_international_flow(self):
with _mock_request_call():
super().dhl_basic_international_flow()
def test_03_dhl_multipackage_international_flow(self):
with _mock_request_call():
super().dhl_multipackage_international_flow()
def test_04_dhl_flow_from_delivery_order(self):
with _mock_request_call():
super().dhl_flow_from_delivery_order()
def test_05_dhl_test_export_declaration_rounding(self):
"""Test that the rounding respects the DHL requirements"""
picking = self.env['stock.picking'].create({
'partner_id': self.delta_pc.id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'move_ids': [
Command.create({
'product_id': self.iPadMini.id,
'product_uom_qty': 7,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
}),
Command.create({
'product_id': self.large_desk.id,
'product_uom_qty': 7,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
})
]
})
picking.action_confirm()
picking.move_line_ids[0].sale_price = 11.43
picking.move_line_ids[1].sale_price = 6.666666600000001
dhl_provider = DHLProvider(self.delivery_carrier_dhl_eu_intl)
export_declaration = dhl_provider._get_export_declaration_vals(self.delivery_carrier_dhl_eu_intl, picking)
item_1, item_2 = export_declaration['lineItems']
self.assertEqual(item_1['price'], 1.633)
self.assertEqual(item_2['price'], 0.952)
def test_06_dhl_weight_conversion_rounding(self):
"""Test that the weight conversion method correctly rounds according to DHL requirements"""
weight_with_precision_issue = 0.7000000000000001
rounded_weight_metric = self.delivery_carrier_dhl_eu_intl._dhl_rest_convert_weight(weight_with_precision_issue)
self.assertEqual(rounded_weight_metric, 0.7)
weight_needs_rounding = 1.23456789
rounded_weight_precise = self.delivery_carrier_dhl_eu_intl._dhl_rest_convert_weight(weight_needs_rounding)
self.assertEqual(rounded_weight_precise, 1.235)
def test_07_dhl_specific_label_format_request(self):
'''
Ensure that label format settings of the delivery method are correctly communicated in the
requests.
'''
def label_format_and_template_check_1(endpoint, payload):
# ZPL format, ECOM26_84_001 and not A4 label
if endpoint == 'shipments':
properties = payload['outputImageProperties']
self.assertEqual(properties['encodingFormat'], 'zpl')
self.assertEqual(properties['imageOptions'], [{
'typeCode': 'label',
'templateName': 'ECOM26_84_001',
'fitLabelsToA4': False,
}])
def label_format_and_template_check_2(endpoint, payload):
# PDF format, ECOM26_A6_002 and A4 label
if endpoint == 'shipments':
properties = payload['outputImageProperties']
self.assertEqual(properties['encodingFormat'], 'pdf')
self.assertEqual(properties['imageOptions'], [{
'typeCode': 'label',
'templateName': 'ECOM26_A6_002',
'fitLabelsToA4': True,
}])
sale_order_1, sale_order_2 = self.env['sale.order'].create([
{
'date_order': self.delivery_date,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'partner_id': self.delta_pc.id,
'order_line': [Command.create({
'product_id': self.iPadMini.id,
'name': "[A1232] Large Cabinet",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': self.iPadMini.lst_price,
})],
},
{
'date_order': self.delivery_date,
'carrier_id': self.delivery_carrier_dhl_eu_intl.id,
'partner_id': self.delta_pc.id,
'order_line': [Command.create({
'product_id': self.iPadMini.id,
'name': "[A1232] Large Cabinet",
'product_uom_id': self.uom_unit.id,
'product_uom_qty': 2.0,
'price_unit': self.iPadMini.lst_price,
})],
},
])
self.delivery_carrier_dhl_eu_intl.dhl_label_image_format = 'ZPL2'
self.delivery_carrier_dhl_eu_intl.dhl_label_template = '8X4_thermal'
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_order_1.id,
'default_carrier_id': self.delivery_carrier_dhl_eu_intl.id
}))
with _mock_request_call(label_format_and_template_check_1):
choose_delivery_carrier = delivery_wizard.save()
choose_delivery_carrier.update_price()
choose_delivery_carrier.button_confirm()
sale_order_1.action_confirm()
self.assertEqual(len(sale_order_1.picking_ids), 1)
picking = sale_order_1.picking_ids[0]
picking.move_ids[0].quantity = 1.0
picking.move_ids[0].picked = True
picking.scheduled_date = self.delivery_date
picking._action_done()
self.assertIsNot(picking.carrier_tracking_ref, False)
self.delivery_carrier_dhl_eu_intl.dhl_label_image_format = 'PDF'
self.delivery_carrier_dhl_eu_intl.dhl_label_template = '6X4_A4_PDF'
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_order_2.id,
'default_carrier_id': self.delivery_carrier_dhl_eu_intl.id
}))
with _mock_request_call(label_format_and_template_check_2):
choose_delivery_carrier = delivery_wizard.save()
choose_delivery_carrier.update_price()
choose_delivery_carrier.button_confirm()
sale_order_2.action_confirm()
self.assertEqual(len(sale_order_2.picking_ids), 1)
picking = sale_order_2.picking_ids[0]
picking.move_ids[0].quantity = 1.0
picking.move_ids[0].picked = True
picking.scheduled_date = self.delivery_date
picking._action_done()
self.assertIsNot(picking.carrier_tracking_ref, False)
def test_dhl_basic_international_flow_multi_company(self):
def commercial_invoice_data_check(endpoint, payload):
if endpoint == 'shipments':
invoice_number_string = payload['content']['exportDeclaration']['invoice']['number']
self.assertIsInstance(invoice_number_string, str)
invoice_number_int = int(''.join(filter(str.isnumeric, invoice_number_string)))
sequence = self.env['ir.sequence'].search([('code', '=', 'delivery_dhl_rest.commercial_invoice')])
self.assertEqual(invoice_number_int + sequence.number_increment, sequence.number_next_actual)
with _mock_request_call(commercial_invoice_data_check):
self.patch(self, 'env', self.uk_co.env)
super().dhl_basic_international_flow()