# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from lxml.etree import fromstring from datetime import datetime, date, timedelta from lxml import etree from odoo.tools.zeep import Client, Plugin from odoo.tools.zeep.wsdl.utils import etree_to_string from odoo import _ from odoo import release from odoo.exceptions import UserError from odoo.tools import float_repr, float_round from odoo.tools.misc import file_path class DHLProvider(): def __init__(self, debug_logger, request_type='ship', prod_environment=False): self.debug_logger = debug_logger if not prod_environment: self.url = 'https://xmlpitest-ea.dhl.com/XMLShippingServlet?isUTF8Support=true' else: self.url = 'https://xmlpi-ea.dhl.com/XMLShippingServlet?isUTF8Support=true' if request_type == "ship": self.client = self._set_client('ship-10.0.wsdl', 'Ship') self.factory = self.client.type_factory('ns1') elif request_type =="rate": self.client = self._set_client('rate.wsdl', 'Rate') self.factory = self.client.type_factory('ns1') self.factory_dct_request = self.client.type_factory('ns2') self.factory_dct_response = self.client.type_factory('ns3') def _set_client(self, wsdl_filename, api): wsdl_path = file_path(f'delivery_dhl/api/{wsdl_filename}') client = Client(wsdl_path) return client def _set_request(self, site_id, password): request = self.factory.Request() service_header = self.factory.ServiceHeader() service_header.MessageTime = datetime.now() service_header.MessageReference = 'ref:' + datetime.now().isoformat() #CHANGEME service_header.SiteID = site_id service_header.Password = password request.ServiceHeader = service_header metadata = self.factory.MetaData() metadata.SoftwareName = release.product_name metadata.SoftwareVersion = release.series request.MetaData = metadata return request def _set_region_code(self, region_code): return region_code def _set_requested_pickup_time(self, requested_pickup): if requested_pickup: return "Y" else: return "N" def _set_billing(self, shipper_account, payment_type, duty_payment_type, is_dutiable): billing = self.factory.Billing() billing.ShipperAccountNumber = shipper_account billing.ShippingPaymentType = payment_type if is_dutiable: billing.DutyPaymentType = duty_payment_type return billing def _set_consignee(self, partner_id): consignee = self.factory.Consignee() consignee.CompanyName = partner_id.commercial_company_name or partner_id.name consignee.AddressLine1 = partner_id.street or partner_id.street2 consignee.AddressLine2 = partner_id.street and partner_id.street2 or None consignee.City = partner_id.city if partner_id.state_id: consignee.Division = partner_id.state_id.name consignee.DivisionCode = partner_id.state_id.code consignee.PostalCode = partner_id.zip consignee.CountryCode = partner_id.country_id.code consignee.CountryName = partner_id.country_id.name contact = self.factory.Contact() contact.PersonName = partner_id.name contact.PhoneNumber = partner_id.phone if partner_id.email: contact.Email = partner_id.email consignee.Contact = contact return consignee def _set_dct_to(self, partner_id): to = self.factory_dct_request.DCTTo() country_code = partner_id.country_id.code zip_code = partner_id.zip or '' if country_code == 'ES' and (zip_code.startswith('35') or zip_code.startswith('38')): country_code = 'IC' to.CountryCode = country_code to.Postalcode = zip_code to.City = partner_id.city return to def _set_shipper(self, account_number, company_partner_id, warehouse_partner_id): shipper = self.factory.Shipper() shipper.ShipperID = account_number shipper.CompanyName = company_partner_id.name shipper.AddressLine1 = warehouse_partner_id.street or warehouse_partner_id.street2 shipper.AddressLine2 = warehouse_partner_id.street and warehouse_partner_id.street2 or None shipper.City = warehouse_partner_id.city if warehouse_partner_id.state_id: shipper.Division = warehouse_partner_id.state_id.name shipper.DivisionCode = warehouse_partner_id.state_id.code shipper.PostalCode = warehouse_partner_id.zip shipper.CountryCode = warehouse_partner_id.country_id.code shipper.CountryName = warehouse_partner_id.country_id.name contact = self.factory.Contact() contact.PersonName = warehouse_partner_id.name contact.PhoneNumber = warehouse_partner_id.phone if warehouse_partner_id.email: contact.Email = warehouse_partner_id.email shipper.Contact = contact return shipper def _set_dct_from(self, warehouse_partner_id): dct_from = self.factory_dct_request.DCTFrom() dct_from.CountryCode = warehouse_partner_id.country_id.code dct_from.Postalcode = warehouse_partner_id.zip dct_from.City = warehouse_partner_id.city return dct_from def _set_dutiable(self, total_value, currency_name, incoterm): dutiable = self.factory.Dutiable() dutiable.DeclaredValue = float_repr(total_value, 2) dutiable.DeclaredCurrency = currency_name if not incoterm: raise UserError(_("Please define an incoterm in the associated sale order or set a default incoterm for the company in the accounting's settings.")) dutiable.TermsOfTrade = incoterm.code return dutiable def _set_dct_dutiable(self, total_value, currency_name): dct_dutiable = self.factory_dct_request.DCTDutiable() dct_dutiable.DeclaredCurrency = currency_name dct_dutiable.DeclaredValue = total_value return dct_dutiable def _set_dct_bkg_details(self, carrier, packages): bkg_details = self.factory_dct_request.BkgDetailsType() bkg_details.PaymentCountryCode = packages[0].company_id.partner_id.country_id.code bkg_details.Date = date.today() bkg_details.ReadyTime = timedelta(hours=1,minutes=2) bkg_details.DimensionUnit = "CM" if carrier.dhl_package_dimension_unit == "C" else "IN" bkg_details.WeightUnit = "KG" if carrier.dhl_package_weight_unit == "K" else "LB" bkg_details.InsuredValue = float_repr(sum(pkg.total_cost for pkg in packages) * carrier.shipping_insurance / 100, precision_digits=3) bkg_details.InsuredCurrency = packages[0].currency_id.name pieces = [] for sequence, package in enumerate(packages): piece = self.factory_dct_request.PieceType() piece.PieceID = sequence piece.PackageTypeCode = package.packaging_type piece.Height = package.dimension['height'] piece.Depth = package.dimension['length'] piece.Width = package.dimension['width'] piece.Weight = carrier._dhl_convert_weight(package.weight, carrier.dhl_package_weight_unit) pieces.append(piece) bkg_details.Pieces = {'Piece': pieces} bkg_details.PaymentAccountNumber = carrier.dhl_account_number if carrier.dhl_dutiable: bkg_details.IsDutiable = "Y" else: bkg_details.IsDutiable = "N" bkg_details.NetworkTypeCode = "AL" return bkg_details def _set_shipment_details(self, picking): shipment_details = self.factory.ShipmentDetails() pieces = [] packages = picking.carrier_id._get_packages_from_picking(picking, picking.carrier_id.dhl_default_package_type_id) for sequence, package in enumerate(packages): piece = self.factory.Piece() piece.PieceID = sequence piece.Height = package.dimension['height'] piece.Depth = package.dimension['length'] piece.Width = package.dimension['width'] piece.Weight = picking.carrier_id._dhl_convert_weight(package.weight, picking.carrier_id.dhl_package_weight_unit) piece.PieceContents = package.name pieces.append(piece) shipment_details.Pieces = self.factory.Pieces(pieces) shipment_details.WeightUnit = picking.carrier_id.dhl_package_weight_unit shipment_details.GlobalProductCode = picking.carrier_id.dhl_product_code shipment_details.LocalProductCode = picking.carrier_id.dhl_product_code shipment_details.Date = date.today() shipment_details.Contents = "MY DESCRIPTION" shipment_details.DimensionUnit = picking.carrier_id.dhl_package_dimension_unit shipment_details.InsuredAmount = float_repr(sum(pkg.total_cost for pkg in packages) * picking.carrier_id.shipping_insurance / 100, precision_digits=2) if picking.carrier_id.dhl_dutiable: shipment_details.IsDutiable = "Y" currency = picking.group_id.sale_id.currency_id or picking.company_id.currency_id shipment_details.CurrencyCode = currency.name return shipment_details def _set_label_image_format(self, label_image_format): return label_image_format def _set_label(self, label): label_obj = self.factory.Label() label_obj.LabelTemplate = label return label_obj def _set_return(self): return_service = self.factory.SpecialService() return_service.SpecialServiceType = "PV" return return_service def _set_insurance(self, shipment_details): insurance_service = self.factory.SpecialService() insurance_service.SpecialServiceType = "II" insurance_service.ChargeValue = shipment_details.InsuredAmount insurance_service.CurrencyCode = shipment_details.CurrencyCode return insurance_service def _process_shipment(self, shipment_request): ShipmentRequest = self.client._Client__obj.get_element('ns0:ShipmentRequest') document = etree.Element('root') ShipmentRequest.render(document, shipment_request) request_to_send = etree_to_string(list(document)[0]) headers = {'Content-Type': 'text/xml'} response = self.client._Client__obj.transport.post(self.url, request_to_send, headers=headers) if self.debug_logger: self.debug_logger(request_to_send, 'dhl_shipment_request') self.debug_logger(response.content, 'dhl_shipment_response') response_element_xml = fromstring(response.content) Response = self.client._Client__obj.get_element(response_element_xml.tag) response_zeep = Response.type.parse_xmlelement(response_element_xml) dict_response = {'tracking_number': 0.0, 'price': 0.0, 'currency': False} # This condition handle both 'ShipmentValidateErrorResponse' and # 'ErrorResponse', we could handle them differently if needed as # the 'ShipmentValidateErrorResponse' is something you cannot do, # and 'ErrorResponse' are bad values given in the request. if hasattr(response_zeep.Response, 'Status'): condition = response_zeep.Response.Status.Condition[0] error_msg = "%s: %s" % (condition.ConditionCode, condition.ConditionData) raise UserError(error_msg) return response_zeep def _process_rating(self, rating_request): DCTRequest = self.client._Client__obj.get_element('ns0:DCTRequest') document = etree.Element('root') DCTRequest.render(document, rating_request) request_to_send = etree_to_string(list(document)[0]) headers = {'Content-Type': 'text/xml'} response = self.client._Client__obj.transport.post(self.url, request_to_send, headers=headers) if self.debug_logger: self.debug_logger(request_to_send, 'dhl_rating_request') self.debug_logger(response.content, 'dhl_rating_response') response_element_xml = fromstring(response.content) dict_response = {'tracking_number': 0.0, 'price': 0.0, 'currency': False} # This condition handle both 'ShipmentValidateErrorResponse' and # 'ErrorResponse', we could handle them differently if needed as # the 'ShipmentValidateErrorResponse' is something you cannot do, # and 'ErrorResponse' are bad values given in the request. if response_element_xml.find('GetQuoteResponse') is not None: return response_element_xml else: condition = response_element_xml.find('Response/Status/Condition') error_msg = "%s: %s" % (condition.find('ConditionCode').text, condition.find('ConditionData').text) raise UserError(error_msg) def check_required_value(self, carrier, recipient, shipper, order=False, picking=False): carrier = carrier.sudo() recipient_required_field = ['city', 'zip', 'country_id'] if not carrier.dhl_SiteID: return _("DHL Site ID is missing, please modify your delivery method settings.") if not carrier.dhl_password: return _("DHL password is missing, please modify your delivery method settings.") if not carrier.dhl_account_number: return _("DHL account number is missing, please modify your delivery method settings.") # The street isn't required if we compute the rate with a partial delivery address in the # express checkout flow. if not recipient.street and not recipient.street2 and not recipient._context.get( 'express_checkout_partial_delivery_address', False ): recipient_required_field.append('street') res = [field for field in recipient_required_field if not recipient[field]] if res: return _("The address of the customer is missing or wrong (Missing field(s) :\n %s)", ", ".join(res).replace("_id", "")) shipper_required_field = ['city', 'zip', 'phone', 'country_id'] if not shipper.street and not shipper.street2: shipper_required_field.append('street') res = [field for field in shipper_required_field if not shipper[field]] if res: return _("The address of your company warehouse is missing or wrong (Missing field(s) :\n %s)", ", ".join(res).replace("_id", "")) if order: if not order.order_line: return _("Please provide at least one item to ship.") error_lines = order.order_line.filtered(lambda line: not line.product_id.weight and not line.is_delivery and line.product_id.type != 'service' and not line.display_type) if error_lines: return _("The estimated shipping price cannot be computed because the weight is missing for the following product(s): \n %s", ", ".join(error_lines.product_id.mapped('name'))) return False def _set_export_declaration(self, carrier, picking, is_return=False): export_lines = [] move_lines = picking.move_line_ids.filtered(lambda line: line.product_id.type in ['product', 'consu']) currency_id = picking.sale_id and picking.sale_id.currency_id or picking.company_id.currency_id for sequence, line in enumerate(move_lines, start=1): if line.move_id.sale_line_id: unit_quantity = line.product_uom_id._compute_quantity(line.quantity, line.move_id.sale_line_id.product_uom) else: unit_quantity = line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) rounded_qty = max(1, float_round(unit_quantity, precision_digits=0, rounding_method='HALF-UP')) item = self.factory.ExportLineItem() item.LineNumber = sequence item.Quantity = int(rounded_qty) item.QuantityUnit = 'PCS' # Pieces - very generic if len(line.product_id.name) > 75: raise UserError(_("DHL doesn't support products with name greater than 75 characters.")) item.Description = line.product_id.name item.Value = float_repr(line.sale_price / rounded_qty, currency_id.decimal_places) item.Weight = item.GrossWeight = { "Weight": carrier._dhl_convert_weight(line.product_id.weight, carrier.dhl_package_weight_unit), "WeightUnit": carrier.dhl_package_weight_unit, } item.ManufactureCountryCode = line.product_id.country_of_origin.code or line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code if line.product_id.hs_code: item.ImportCommodityCode = line.product_id.hs_code item.CommodityCode = line.product_id.hs_code export_lines.append(item) export_declaration = self.factory.ExportDeclaration() export_declaration.InvoiceDate = datetime.today() export_declaration.InvoiceNumber = carrier.env['ir.sequence'].sudo().next_by_code('delivery_dhl.commercial_invoice') if is_return: export_declaration.ExportReason = 'RETURN' export_declaration.ExportLineItem = export_lines if picking.sale_id.client_order_ref: export_declaration.ReceiverReference = picking.sale_id.client_order_ref return export_declaration