From cd02f70adc2942ea084f933c053eb8f7543c2e83 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 01:00:05 +0000 Subject: [PATCH] [IMP] delivery_puralator: refactor multi to use full rating, Additionally, use weight or volume package type finder Refactor single use API (purolator_rate_shipment) to use the multi API (_purolator_rate_shipment_multi_package) and find itself. --- .../models/delivery_purolator.py | 146 +++++++++--------- .../models/purolator_services.py | 97 ++++++++++-- 2 files changed, 151 insertions(+), 92 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 74cd81eb..d1999cb2 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -93,48 +93,26 @@ class ProviderPurolator(models.Model): return weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False) def purolator_convert_length(self, length): - volume_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() - return weight_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False) + raise Exception('Not implemented. Need to do math on UOM to convert less dimensions') + volume_uom_id = self.env['product.template']._get_volume_uom_id_from_ir_config_parameter() + return volume_uom_id._compute_quantity(weight, self.env.ref('uom.product_uom_lb'), round=False) - def purolator_rate_shipment(self, order): - # sudoself = self.sudo() - third_party = self.purolator_third_party(order=order) - sender = self.get_shipper_warehouse(order=order) - receiver = self.get_recipient(order=order) - receiver_address = { - 'City': receiver.city, - 'Province': receiver.state_id.code, - 'Country': receiver.country_id.code, - 'PostalCode': receiver.zip, - } - # TODO packaging volume/length/width/height - weight = self.purolator_convert_weight(order._get_estimated_weight()) - service = self._purolator_service() - res = service.get_quick_estimate( - sender.zip, - receiver_address, - self.purolator_default_package_type_id.shipper_package_code, - weight, - ) - if res['error']: - return { - 'success': False, - 'price': 0.0, - 'error_message': _(res['error']), - 'warning_message': False, - } - shipment = list(filter(lambda s: s['ServiceID'] == self.purolator_service_type, res['shipments'])) - if not shipment: - return { - 'success': False, - 'price': 0.0, - 'error_message': _('No rate found matching service: %s') % self.purolator_service_type, - 'warning_message': False, - } + def purolator_rate_shipment(self, order, downgrade_response=True): + multi_res = self._purolator_rate_shipment_multi_package(order=order) + for res in multi_res: + if res.get('carrier') == self: + if downgrade_response: + return { + 'success': True, + 'price': res.get('price', 0.0), + 'error_message': False, + 'warning_message': False, + } + return res return { - 'success': True, - 'price': shipment[0]['TotalPrice'] if not third_party else 0.0, - 'error_message': False, + 'success': False, + 'price': 0.0, + 'error_message': _('No rate found matching service: %s') % self.purolator_service_type, 'warning_message': False, } @@ -147,45 +125,71 @@ class ProviderPurolator(models.Model): rates += self._purolator_rate_shipment_multi_package(order=order, picking=picking, package=package) return rates + def _purolator_format_errors(self, response_body, raise_class=None): + errors = response_body.ResponseInformation.Errors + if errors: + errors = errors.Error # unpack container node + puro_errors = '\n\n'.join(['%s - %s - %s' % (e.Code, e.AdditionalInformation, e.Description) for e in errors]) + if raise_class: + raise raise_class(_('Error(s) during Purolator Request:\n%s') % (puro_errors, )) + return puro_errors + + def _purolator_shipment_fill_payor(self, request, picking=None, order=None): + request.PaymentInformation.PaymentType = 'Sender' + request.PaymentInformation.RegisteredAccountNumber = self.purolator_account_number + request.PaymentInformation.BillingAccountNumber = self.purolator_account_number + third_party_account = self.purolator_third_party(picking=picking, order=order) + # when would it be 'Receiver' ? + if third_party_account: + request.PaymentInformation.PaymentType = 'ThirdParty' + request.PaymentInformation.BillingAccountNumber = third_party_account + def _purolator_rate_shipment_multi_package(self, order=None, picking=None, package=None): + service = self._purolator_service() third_party = self.purolator_third_party(order=order, picking=picking) sender = self.get_shipper_warehouse(order=order, picking=picking) receiver = self.get_recipient(order=order, picking=picking) - receiver_address = { - 'City': receiver.city, - 'Province': receiver.state_id.code, - 'Country': receiver.country_id.code, - 'PostalCode': receiver.zip, - } - weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() - volume_uom_id = self.env['product.template']._get_volume_uom_id_from_ir_config_parameter() - date_planned = fields.Datetime.now() + date_planned = fields.Date.today() if self.env.context.get('date_planned'): date_planned = self.env.context.get('date_planned') + if hasattr(date_planned, 'date'): + # this should be a datetime + date_planned = date_planned.date() + + # create SOAP request to fill in + shipment = service.estimate_shipment_request() + # request getting more than one service back + shipment.ShowAlternativeServicesIndicator = "true" + # indicate when we will ship this for time in transit + shipment.ShipmentDate = str(date_planned) + + # populate origin information + self._purolator_fill_address(shipment.SenderInformation.Address, sender) + # populate destination + self._purolator_fill_address(shipment.ReceiverInformation.Address, receiver) - # TODO need packaging volume/dimensions - package_code = self.purolator_default_package_type_id.shipper_package_code if order: - weight = order._get_estimated_weight() + service.estimate_shipment_add_sale_order_packages(shipment, self, order) else: - if package: - weight = package.shipping_weight - package_code = package.package_type_id.shipper_package_code if package.package_type_id.package_carrier_type == 'purolator' else package_code - else: - weight = picking.shipping_weight or picking.weight - weight = self.purolator_convert_weight(weight) - service = self._purolator_service() - res = service.get_quick_estimate(sender.zip, receiver_address, package_code, weight) - if res['error']: + service.estimate_shipment_add_picking_packages(shipment, self, picking, package) + + self._purolator_shipment_fill_payor(shipment, order=order, picking=picking) + + shipment_res = service.get_full_estimate(shipment) + + # _logger.info('_purolator_rate_shipment_multi_package called with shipment %s result %s' % (shipment, shipment_res)) + + errors = self._purolator_format_errors(shipment_res) + if errors: return [{'carrier': self, 'success': False, 'price': 0.0, - 'error_message': _('Error:\n%s') % res['error'], + 'error_message': '\n'.join(errors), 'warning_message': False, }] rates = [] - for shipment in res['shipments']: + for shipment in shipment_res.ShipmentEstimates.ShipmentEstimate: carrier = self.purolator_find_delivery_carrier_for_service(shipment['ServiceID']) if carrier: price = shipment['TotalPrice'] @@ -297,24 +301,14 @@ class ProviderPurolator(models.Model): # $request->Shipment->PackageInformation->OptionsInformation->Options->OptionIDValuePair->ID = "ResidentialSignatureDomestic"; # $request->Shipment->PackageInformation->OptionsInformation->Options->OptionIDValuePair->Value = "true"; - shipment.PaymentInformation.PaymentType = 'Sender' - shipment.PaymentInformation.RegisteredAccountNumber = self.purolator_account_number - shipment.PaymentInformation.BillingAccountNumber = self.purolator_account_number - third_party_account = self.purolator_third_party(picking=picking) - # when would it be 'Receiver' ? - if third_party_account: - shipment.PaymentInformation.PaymentType = 'ThirdParty' - shipment.PaymentInformation.BillingAccountNumber = third_party_account + self._purolator_shipment_fill_payor(shipment, picking=picking) shipment_res = service.shipment_create(shipment, printer_type=('Regular' if self.purolator_label_file_type == 'PDF' else 'Thermal')) # _logger.info('purolator service.shipment_create for shipment %s resulted in %s' % (shipment, shipment_res)) - errors = shipment_res.ResponseInformation.Errors - if errors: - errors = errors.Error # unpack container node - puro_errors = '\n\n'.join(['%s - %s - %s' % (e.Code, e.AdditionalInformation, e.Description) for e in errors]) - raise UserError(_('Error(s) during Purolator Shipment Request:\n%s') % (puro_errors, )) + # this will raise an error alerting the user if there is an error, and no more + self._purolator_format_errors(shipment_res, raise_class=UserError) document_blobs = [] shipment_pin = shipment_res.ShipmentPIN.Value diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index 0618a6cd..417a0823 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -1,3 +1,4 @@ +from math import ceil from requests import Session from requests.auth import HTTPBasicAuth from zeep import Client @@ -81,7 +82,14 @@ class PurolatorClient(object): UserToken=self.activation_key, ) client.set_default_soapheaders([header_value]) - return client + return client + + def get_full_estimate(self, shipment, show_alternative_services='true'): + response = self.estimating_client.service.GetFullEstimate( + Shipment=shipment, + ShowAlternativeServicesIndicator=show_alternative_services, + ) + return response.body def get_quick_estimate(self, sender_postal_code, receiver_address, package_type, total_weight): """ Call GetQuickEstimate @@ -123,26 +131,83 @@ class PurolatorClient(object): } def shipment_request(self): - shipment = self.shipping_factory.Shipment() - shipment.SenderInformation = self.shipping_factory.SenderInformation() - shipment.SenderInformation.Address = self.shipping_factory.Address() - shipment.SenderInformation.Address.PhoneNumber = self.shipping_factory.PhoneNumber() - shipment.ReceiverInformation = self.shipping_factory.ReceiverInformation() - shipment.ReceiverInformation.Address = self.shipping_factory.Address() - shipment.ReceiverInformation.Address.PhoneNumber = self.shipping_factory.PhoneNumber() - shipment.PackageInformation = self.shipping_factory.PackageInformation() - shipment.PackageInformation.TotalWeight = self.shipping_factory.TotalWeight() - shipment.PackageInformation.PiecesInformation = self.shipping_factory.ArrayOfPiece() - shipment.PaymentInformation = self.shipping_factory.PaymentInformation() + return self._shipment_request(self.shipping_factory) + + # just like above, but using estimate api + def estimate_shipment_request(self): + return self._shipment_request(self.estimating_factory) + + def _shipment_request(self, factory): + shipment = factory.Shipment() + shipment.SenderInformation = factory.SenderInformation() + shipment.SenderInformation.Address = factory.Address() + shipment.SenderInformation.Address.PhoneNumber = factory.PhoneNumber() + shipment.ReceiverInformation = factory.ReceiverInformation() + shipment.ReceiverInformation.Address = factory.Address() + shipment.ReceiverInformation.Address.PhoneNumber = factory.PhoneNumber() + shipment.PackageInformation = factory.PackageInformation() + shipment.PackageInformation.TotalWeight = factory.TotalWeight() + shipment.PackageInformation.PiecesInformation = factory.ArrayOfPiece() + shipment.PaymentInformation = factory.PaymentInformation() return shipment - def shipment_add_picking_packages(self, shipment, carrier, picking, packages): + def estimate_shipment_add_sale_order_packages(self, shipment, carrier, order): + # this could be a non-purolator package type as returned by the search functions + package_type = carrier.get_package_type_for_order(order) + shipment.PackageInformation.ServiceID = carrier.purolator_service_type + weight = carrier.purolator_convert_weight(order._get_estimated_weight()) + package_type_max_weight = 0.0 + if package_type.max_weight: + package_type_max_weight = carrier.purolator_convert_weight(package_type.max_weight) + + if package_type_max_weight and weight > package_type_max_weight: + total_pieces = ceil(weight / package_type_max_weight) + package_weight = weight / total_pieces + else: + total_pieces = 1 + package_weight = weight + + if package_weight < 1.0: + package_weight = 1.0 + + total_weight_value = package_weight * total_pieces + for _i in range(total_pieces): + p = self.estimating_factory.Piece( + Weight={ + 'Value': str(package_weight), + 'WeightUnit': 'lb', + }, + Length={ + 'Value': str(package_type.packaging_length), # TODO need conversion + 'DimensionUnit': 'in', + }, + Width={ + 'Value': str(package_type.width), # TODO need conversion + 'DimensionUnit': 'in', + }, + Height={ + 'Value': str(package_type.height), # TODO need conversion + 'DimensionUnit': 'in', + }, + ) + shipment.PackageInformation.PiecesInformation.Piece.append(p) + shipment.PackageInformation.TotalWeight.Value = str(weight) + shipment.PackageInformation.TotalWeight.WeightUnit = 'lb' + shipment.PackageInformation.TotalPieces = str(total_pieces) + + def estimate_shipment_add_picking_packages(self, shipment, carrier, picking, packages): + return self._shipment_add_picking_packages(self.estimating_factory, shipment, carrier, picking, packages) + + def shipment_add_picking_packages(self, shipment, carrier, picking, packages): + return self._shipment_add_picking_packages(self.shipping_factory, shipment, carrier, picking, packages) + + def _shipment_add_picking_packages(self, factory, shipment, carrier, picking, packages): # note that no package can be less than 1lb, so we fix that here... # for the package to be allowed, it must be the same service shipment.PackageInformation.ServiceID = carrier.purolator_service_type total_weight_value = 0.0 - total_pieces = len(packages) or 1 + total_pieces = len(packages or []) or 1 if not packages: # setup default package package_weight = carrier.purolator_convert_weight(picking.shipping_weight) @@ -150,7 +215,7 @@ class PurolatorClient(object): package_weight = 1.0 total_weight_value += package_weight package_type = carrier.purolator_default_package_type_id - p = self.shipping_factory.Piece( + p = factory.Piece( Weight={ 'Value': str(package_weight), 'WeightUnit': 'lb', @@ -176,7 +241,7 @@ class PurolatorClient(object): package_weight = 1.0 package_type = package.package_type_id total_weight_value += package_weight - p = self.shipping_factory.Piece( + p = factory.Piece( Weight={ 'Value': str(package_weight), 'WeightUnit': 'lb',