# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from .dhl_request import DHLProvider from markupsafe import Markup from odoo.tools.zeep.helpers import serialize_object from odoo import api, models, fields, _ from odoo.exceptions import UserError from odoo.tools import float_repr from odoo.tools.safe_eval import const_eval class Providerdhl(models.Model): _inherit = 'delivery.carrier' delivery_type = fields.Selection(selection_add=[ ('dhl', "DHL") ], ondelete={'dhl': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0})}) dhl_SiteID = fields.Char(string="DHL SiteID", groups="base.group_system") dhl_password = fields.Char(string="DHL Password", groups="base.group_system") dhl_account_number = fields.Char(string="DHL Account Number", groups="base.group_system") dhl_package_dimension_unit = fields.Selection([('I', 'Inches'), ('C', 'Centimeters')], default='C', string='Package Dimension Unit') dhl_package_weight_unit = fields.Selection([('L', 'Pounds'), ('K', 'Kilograms')], default='K', string="Package Weight Unit") dhl_default_package_type_id = fields.Many2one('stock.package.type', string='DHL Package Type') dhl_region_code = fields.Selection([('AP', 'Asia Pacific'), ('AM', 'America'), ('EU', 'Europe')], default='AM', string='Region') # Nowadays hidden, by default it's the D, couldn't find any documentation on other services dhl_product_code = fields.Selection([('0', '0 - Logistics Services'), ('1', '1 - Domestic Express 12:00'), ('2', '2 - B2C'), ('3', '3 - B2C'), ('4', '4 - Jetline'), ('5', '5 - Sprintline'), ('6', '6 - Secureline'), ('7', '7 - Express Easy'), ('8', '8 - Express Easy'), ('9', '9 - Europack'), ('A', 'A - Auto Reversals'), ('B', 'B - Break Bulk Express'), ('C', 'C - Medical Express'), ('D', 'D - Express Worldwide'), ('E', 'E - Express 9:00'), ('F', 'F - Freight Worldwide'), ('G', 'G - Domestic Economy Select'), ('H', 'H - Economy Select'), ('I', 'I - Break Bulk Economy'), ('J', 'J - Jumbo Box'), ('K', 'K - Express 9:00'), ('L', 'L - Express 10:30'), ('M', 'M - Express 10:30'), ('N', 'N - Domestic Express'), ('O', 'O - DOM Express 10:30'), ('P', 'P - Express Worldwide'), ('Q', 'Q - Medical Express'), ('R', 'R - GlobalMail Business'), ('S', 'S - Same Day'), ('T', 'T - Express 12:00'), ('U', 'U - Express Worldwide'), ('V', 'V - Europack'), ('W', 'W - Economy Select'), ('X', 'X - Express Envelope'), ('Y', 'Y - Express 12:00'), ('Z', 'Z - Destination Charges'), ], default='D', string='DHL Product') dhl_dutiable = fields.Boolean(string="Dutiable Material", help="Check this if your package is dutiable.") dhl_duty_payment = fields.Selection([('S', 'Sender'), ('R', 'Recipient')], required=True, default="S") dhl_label_image_format = fields.Selection([ ('EPL2', 'EPL2'), ('PDF', 'PDF'), ('ZPL2', 'ZPL2'), ], string="Label Image Format", default='PDF') dhl_label_template = fields.Selection([ ('8X4_A4_PDF', '8X4_A4_PDF'), ('8X4_thermal', '8X4_thermal'), ('8X4_A4_TC_PDF', '8X4_A4_TC_PDF'), ('6X4_thermal', '6X4_thermal'), ('6X4_A4_PDF', '6X4_A4_PDF'), ('8X4_CI_PDF', '8X4_CI_PDF'), ('8X4_CI_thermal', '8X4_CI_thermal'), ('8X4_RU_A4_PDF', '8X4_RU_A4_PDF'), ('6X4_PDF', '6X4_PDF'), ('8X4_PDF', '8X4_PDF') ], string="Label Template", default='8X4_A4_PDF') dhl_custom_data_request = fields.Text( 'Custom data for DHL requests,', help="""The custom data in DHL is organized like the inside of a json file. There are 3 possible keys: 'rate', 'ship', 'return', to which you can add your custom data. More info on https://xmlportal.dhl.com/""" ) @api.ondelete(at_uninstall=False) def _unlink_except_commercial_invoice_sequence(self): if self.env.ref('delivery_dhl.dhl_commercial_invoice_seq').id in self.ids: raise UserError(_('You cannot delete the commercial invoice sequence.')) def _compute_can_generate_return(self): super(Providerdhl, self)._compute_can_generate_return() for carrier in self: if carrier.delivery_type == 'dhl': carrier.can_generate_return = True def _compute_supports_shipping_insurance(self): super(Providerdhl, self)._compute_supports_shipping_insurance() for carrier in self: if carrier.delivery_type == 'dhl': carrier.supports_shipping_insurance = True def dhl_rate_shipment(self, order): res = self._rate_shipment_vals(order=order) return res def _rate_shipment_vals(self, order=False, picking=False): if picking: warehouse_partner_id = picking.picking_type_id.warehouse_id.partner_id currency_id = picking.sale_id.currency_id or picking.company_id.currency_id destination_partner_id = picking.partner_id total_value = sum(sml.sale_price for sml in picking.move_line_ids) else: warehouse_partner_id = order.warehouse_id.partner_id currency_id = order.currency_id or order.company_id.currency_id total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in order.order_line.filtered(lambda l: l.product_id.type in ('consu', 'product') and not l.display_type)) destination_partner_id = order.partner_shipping_id rating_request = {} srm = DHLProvider(self.log_xml, request_type="rate", prod_environment=self.prod_environment) check_value = srm.check_required_value(self, destination_partner_id, warehouse_partner_id, order=order, picking=picking) if check_value: return {'success': False, 'price': 0.0, 'error_message': check_value, 'warning_message': False} site_id = self.sudo().dhl_SiteID password = self.sudo().dhl_password rating_request['Request'] = srm._set_request(site_id, password) rating_request['From'] = srm._set_dct_from(warehouse_partner_id) if picking: packages = self._get_packages_from_picking(picking, self.dhl_default_package_type_id) else: packages = self._get_packages_from_order(order, self.dhl_default_package_type_id) rating_request['BkgDetails'] = srm._set_dct_bkg_details(self, packages) rating_request['To'] = srm._set_dct_to(destination_partner_id) if self.dhl_dutiable: rating_request['Dutiable'] = srm._set_dct_dutiable(total_value, currency_id.name) real_rating_request = {} real_rating_request['GetQuote'] = rating_request real_rating_request['schemaVersion'] = 2.0 self._dhl_add_custom_data_to_request(rating_request, 'rate') response = srm._process_rating(real_rating_request) available_product_code = [] shipping_charge = False qtd_shp = response.findall('GetQuoteResponse/BkgDetails/QtdShp') if qtd_shp: for q in qtd_shp: charge = q.find('ShippingCharge').text global_product_code = q.find('GlobalProductCode').text if global_product_code == self.dhl_product_code and charge: shipping_charge = charge shipping_currency = q.find('CurrencyCode') shipping_currency = None if shipping_currency is None else shipping_currency.text break else: available_product_code.append(global_product_code) else: condition = response.find('GetQuoteResponse/Note/Condition') if condition: condition_code = condition.find('ConditionCode').text if condition_code == '410301': return { 'success': False, 'price': 0.0, 'error_message': "%s.\n%s" % (condition.find('ConditionData').text, _("Hint: The destination may not require the dutiable option.")), 'warning_message': False, } elif condition_code in ['420504', '420505', '420506', '410304'] or\ response.find('GetQuoteResponse/Note/ActionStatus').text == "Failure": return { 'success': False, 'price': 0.0, 'error_message': "%s." % (condition.find('ConditionData').text), 'warning_message': False, } if shipping_charge: if order: order_currency = order.currency_id else: order_currency = picking.sale_id.currency_id or picking.company_id.currency_id if shipping_currency is None or order_currency.name == shipping_currency: price = float(shipping_charge) else: quote_currency = self.env['res.currency'].search([('name', '=', shipping_currency)], limit=1) price = quote_currency._convert(float(shipping_charge), order_currency, (order or picking).company_id, order.date_order if order else fields.Date.today()) return {'success': True, 'price': price, 'error_message': False, 'warning_message': False} if available_product_code: return {'success': False, 'price': 0.0, 'error_message': _( "There is no price available for this shipping, you should rather try with the DHL product %s", available_product_code[0]), 'warning_message': False} def dhl_send_shipping(self, pickings): res = [] for picking in pickings: shipment_request = {} srm = DHLProvider(self.log_xml, request_type="ship", prod_environment=self.prod_environment) site_id = self.sudo().dhl_SiteID password = self.sudo().dhl_password account_number = self.sudo().dhl_account_number shipment_request['Request'] = srm._set_request(site_id, password) shipment_request['RegionCode'] = srm._set_region_code(self.dhl_region_code) shipment_request['RequestedPickupTime'] = srm._set_requested_pickup_time(True) shipment_request['Billing'] = srm._set_billing(account_number, "S", self.dhl_duty_payment, self.dhl_dutiable) shipment_request['Consignee'] = srm._set_consignee(picking.partner_id) shipment_request['Shipper'] = srm._set_shipper(account_number, picking.company_id.partner_id, picking.picking_type_id.warehouse_id.partner_id) shipment_request['Reference'] = { 'ReferenceID': picking.sale_id.name if picking.sale_id else picking.name, 'ReferenceType': 'CU' } total_value, currency_name = self._dhl_calculate_value(picking) if self.dhl_dutiable: incoterm = picking.sale_id.incoterm or self.env.company.incoterm_id shipment_request['Dutiable'] = srm._set_dutiable(total_value, currency_name, incoterm) if picking._should_generate_commercial_invoice(): shipment_request['UseDHLInvoice'] = 'Y' shipment_request['DHLInvoiceType'] = 'CMI' shipment_request['ExportDeclaration'] = srm._set_export_declaration(self, picking) shipment_request['ShipmentDetails'] = srm._set_shipment_details(picking) shipment_request['LabelImageFormat'] = srm._set_label_image_format(self.dhl_label_image_format) shipment_request['Label'] = srm._set_label(self.dhl_label_template) shipment_request['schemaVersion'] = 10.0 shipment_request['LanguageCode'] = 'en' if picking.carrier_id.shipping_insurance: shipment_request['SpecialService'] = [] shipment_request['SpecialService'].append(srm._set_insurance(shipment_request['ShipmentDetails'])) self._dhl_add_custom_data_to_request(shipment_request, 'ship') dhl_response = srm._process_shipment(shipment_request) traking_number = dhl_response.AirwayBillNumber logmessage = Markup(_("Shipment created into DHL
Tracking Number: %s")) % (traking_number) dhl_labels = [('%s-%s.%s' % (self._get_delivery_label_prefix(), traking_number, self.dhl_label_image_format), dhl_response.LabelImage[0].OutputImage)] dhl_cmi = [('%s-%s.%s' % (self._get_delivery_doc_prefix(), mlabel.DocName, mlabel.DocFormat), mlabel.DocImageVal) for mlabel in dhl_response.LabelImage[0].MultiLabels.MultiLabel] if dhl_response.LabelImage[0].MultiLabels else None lognote_pickings = picking if picking.sale_id: lognote_pickings |= picking.sale_id.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel')) for pick in lognote_pickings: pick.message_post(body=logmessage, attachments=dhl_labels) if dhl_cmi: pick.message_post(body=_("DHL Documents"), attachments=dhl_cmi) shipping_data = { 'exact_price': 0, 'tracking_number': traking_number, } rate = self._rate_shipment_vals(picking=picking) shipping_data['exact_price'] = rate['price'] if self.return_label_on_delivery: self.get_return_label(picking) res = res + [shipping_data] return res def dhl_get_return_label(self, picking, tracking_number=None, origin_date=None): shipment_request = {} srm = DHLProvider(self.log_xml, request_type="ship", prod_environment=self.prod_environment) site_id = self.sudo().dhl_SiteID password = self.sudo().dhl_password account_number = self.sudo().dhl_account_number shipment_request['Request'] = srm._set_request(site_id, password) shipment_request['RegionCode'] = srm._set_region_code(self.dhl_region_code) shipment_request['RequestedPickupTime'] = srm._set_requested_pickup_time(True) shipment_request['Billing'] = srm._set_billing(account_number, "S", "S", self.dhl_dutiable) shipment_request['Consignee'] = srm._set_consignee(picking.picking_type_id.warehouse_id.partner_id) shipment_request['Shipper'] = srm._set_shipper(account_number, picking.partner_id, picking.partner_id) total_value, currency_name = self._dhl_calculate_value(picking) if self.dhl_dutiable: incoterm = picking.sale_id.incoterm or self.env.company.incoterm_id shipment_request['Dutiable'] = srm._set_dutiable(total_value, currency_name, incoterm) if picking._should_generate_commercial_invoice(): shipment_request['UseDHLInvoice'] = 'Y' shipment_request['DHLInvoiceType'] = 'CMI' shipment_request['ExportDeclaration'] = srm._set_export_declaration(self, picking, is_return=True) shipment_request['ShipmentDetails'] = srm._set_shipment_details(picking) shipment_request['LabelImageFormat'] = srm._set_label_image_format(self.dhl_label_image_format) shipment_request['Label'] = srm._set_label(self.dhl_label_template) shipment_request['SpecialService'] = [] shipment_request['SpecialService'].append(srm._set_return()) shipment_request['schemaVersion'] = 10.0 shipment_request['LanguageCode'] = 'en' self._dhl_add_custom_data_to_request(shipment_request, 'return') dhl_response = srm._process_shipment(shipment_request) traking_number = dhl_response.AirwayBillNumber logmessage = Markup(_("Shipment created into DHL
Tracking Number: %s")) % (traking_number) dhl_labels = [('%s-%s-%s.%s' % (self.get_return_label_prefix(), traking_number, 1, self.dhl_label_image_format), dhl_response.LabelImage[0].OutputImage)] dhl_cmi = [('%s-Return-%s.%s' % (self._get_delivery_doc_prefix(), mlabel.DocName, mlabel.DocFormat), mlabel.DocImageVal) for mlabel in dhl_response.LabelImage[0].MultiLabels.MultiLabel] if dhl_response.LabelImage[0].MultiLabels else None lognote_pickings = picking.sale_id.picking_ids if picking.sale_id else picking for pick in lognote_pickings: pick.message_post(body=logmessage, attachments=dhl_labels) if dhl_cmi: pick.message_post(body=_("DHL Documents"), attachments=dhl_cmi) shipping_data = { 'exact_price': 0, 'tracking_number': traking_number, } return shipping_data def dhl_get_tracking_link(self, picking): return 'http://www.dhl.com/en/express/tracking.html?AWB=%s' % picking.carrier_tracking_ref def dhl_cancel_shipment(self, picking): # Obviously you need a pick up date to delete SHIPMENT by DHL. So you can't do it if you didn't schedule a pick-up. picking.message_post(body=_(u"You can't cancel DHL shipping without pickup date.")) picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0}) def _dhl_convert_weight(self, weight, unit): weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() if unit == 'L': weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False) else: weight = weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_kgm'), round=False) return float_repr(weight, 3) def _dhl_add_custom_data_to_request(self, request, request_type): """Adds the custom data to the request. When there are multiple items in a list, they will all be affected by the change. for example, with {"ShipmentDetails": {"Pieces": {"Piece": {"AdditionalInformation": "custom info"}}}} the AdditionalInformation of each piece will be updated. """ if not self.dhl_custom_data_request: return try: custom_data = const_eval('{%s}' % self.dhl_custom_data_request).get(request_type, {}) except SyntaxError: raise UserError(_('Invalid syntax for DHL custom data.')) def extra_data_to_request(request, custom_data): """recursive function that adds custom data to the current request.""" for key, new_value in custom_data.items(): request[key] = current_value = serialize_object(request.get(key, {})) or None if isinstance(current_value, list): for item in current_value: extra_data_to_request(item, new_value) elif isinstance(new_value, dict) and isinstance(current_value, dict): extra_data_to_request(current_value, new_value) else: request[key] = new_value extra_data_to_request(request, custom_data) def _dhl_calculate_value(self, picking): sale_order = picking.sale_id if sale_order: total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in sale_order.order_line.filtered( lambda l: l.product_id.type in ('consu', 'product') and not l.display_type)) currency_name = picking.sale_id.currency_id.name else: total_value = sum([line.product_id.lst_price * line.product_qty for line in picking.move_ids]) currency_name = picking.company_id.currency_id.name return total_value, currency_name