1002 lines
62 KiB
Python
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()
|