From 73e2f144e53fa248c6d5b7ba450788dae6fbb2bf Mon Sep 17 00:00:00 2001 From: Mayank Patel Date: Wed, 11 Sep 2024 05:45:08 +0000 Subject: [PATCH] [REM] delivery_purolator: available in professional H14528 --- delivery_purolator/__init__.py | 1 - delivery_purolator/__manifest__.py | 29 -- .../data/delivery_purolator_data.xml | 24 -- .../data/delivery_purolator_demo.xml | 46 --- delivery_purolator/models/__init__.py | 2 - .../models/delivery_purolator.py | 385 ------------------ .../models/purolator_services.py | 323 --------------- .../models/stock_package_type.py | 7 - delivery_purolator/tests/__init__.py | 1 - delivery_purolator/tests/test_purolator.py | 147 ------- .../views/delivery_purolator_views.xml | 28 -- 11 files changed, 993 deletions(-) delete mode 100644 delivery_purolator/__init__.py delete mode 100644 delivery_purolator/__manifest__.py delete mode 100644 delivery_purolator/data/delivery_purolator_data.xml delete mode 100644 delivery_purolator/data/delivery_purolator_demo.xml delete mode 100644 delivery_purolator/models/__init__.py delete mode 100644 delivery_purolator/models/delivery_purolator.py delete mode 100644 delivery_purolator/models/purolator_services.py delete mode 100644 delivery_purolator/models/stock_package_type.py delete mode 100644 delivery_purolator/tests/__init__.py delete mode 100644 delivery_purolator/tests/test_purolator.py delete mode 100644 delivery_purolator/views/delivery_purolator_views.xml diff --git a/delivery_purolator/__init__.py b/delivery_purolator/__init__.py deleted file mode 100644 index 0650744f..00000000 --- a/delivery_purolator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/delivery_purolator/__manifest__.py b/delivery_purolator/__manifest__.py deleted file mode 100644 index cc305e42..00000000 --- a/delivery_purolator/__manifest__.py +++ /dev/null @@ -1,29 +0,0 @@ -{ - 'name': 'Purolator Shipping', - 'summary': 'Send your shippings through Purolator and track them online.', - 'version': '15.0.1.0.1', - 'author': "Hibou Corp.", - 'category': 'Warehouse', - 'license': 'OPL-1', - 'images': [], - 'website': "https://hibou.io", - 'description': """ -Purolator Shipping -================== - -* Provides estimates on shipping costs. -* Send your shippings and track packages. -""", - 'depends': [ - 'delivery_hibou', - ], - 'demo': [ - 'data/delivery_purolator_demo.xml', - ], - 'data': [ - 'data/delivery_purolator_data.xml', - 'views/delivery_purolator_views.xml', - ], - 'auto_install': False, - 'installable': True, -} diff --git a/delivery_purolator/data/delivery_purolator_data.xml b/delivery_purolator/data/delivery_purolator_data.xml deleted file mode 100644 index 527dd1c0..00000000 --- a/delivery_purolator/data/delivery_purolator_data.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Purolator Default - - purolator - - - - Purolator LargePackage - LargePackage - purolator - - - - Purolator FlatPackage - FlatPackage - purolator - - - - diff --git a/delivery_purolator/data/delivery_purolator_demo.xml b/delivery_purolator/data/delivery_purolator_demo.xml deleted file mode 100644 index ede6dd63..00000000 --- a/delivery_purolator/data/delivery_purolator_demo.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - Purolator Express - Delivery_PurolatorExpress - service - - - - 0.0 - order - - - Purolator Express Test - - purolator - PurolatorExpress - - - - - - Purolator Ground - Delivery_PurolatorGround - service - - - - 0.0 - order - - - Purolator Ground Test - - purolator - PurolatorGround - - - - - - diff --git a/delivery_purolator/models/__init__.py b/delivery_purolator/models/__init__.py deleted file mode 100644 index 8a30199b..00000000 --- a/delivery_purolator/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import delivery_purolator -from . import stock_package_type diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py deleted file mode 100644 index fe9de227..00000000 --- a/delivery_purolator/models/delivery_purolator.py +++ /dev/null @@ -1,385 +0,0 @@ -from base64 import b64decode -from odoo import fields, models, _ -from odoo.exceptions import UserError -from .purolator_services import PurolatorClient -import logging - - -_logger = logging.getLogger(__name__) - -# 2022-09-21 - US Methods are known to rate, but cannot ship without additional customs/documents -PUROLATOR_SERVICES = [ - ('PurolatorExpress9AM', 'Purolator Express 9AM'), - ('PurolatorExpress10:30AM', 'Purolator Express 10:30AM'), - ('PurolatorExpress12PM', 'Purolator Express 12PM'), - ('PurolatorExpress', 'Purolator Express'), - ('PurolatorExpressEvening', 'Purolator Express Evening'), - ('PurolatorExpressEnvelope9AM', 'Purolator Express Envelope 9AM'), - ('PurolatorExpressEnvelope10:30AM', 'Purolator Express Envelope 10:30AM'), - ('PurolatorExpressEnvelope12PM', 'Purolator Express Envelope 12PM'), - ('PurolatorExpressEnvelope', 'Purolator Express Envelope'), - ('PurolatorExpressEnvelopeEvening', 'Purolator Express Envelope Evening'), - ('PurolatorExpressPack9AM', 'Purolator Express Pack 9AM'), - ('PurolatorExpressPack10:30AM', 'Purolator Express Pack 10:30AM'), - ('PurolatorExpressPack12PM', 'Purolator Express Pack 12PM'), - ('PurolatorExpressPack', 'Purolator Express Pack'), - ('PurolatorExpressPackEvening', 'Purolator Express Pack Evening'), - ('PurolatorExpressBox9AM', 'Purolator Express Box 9AM'), - ('PurolatorExpressBox10:30AM', 'Purolator Express Box 10:30AM'), - ('PurolatorExpressBox12PM', 'Purolator Express Box 12PM'), - ('PurolatorExpressBox', 'Purolator Express Box'), - ('PurolatorExpressBoxEvening', 'Purolator Express Box Evening'), - ('PurolatorGround', 'Purolator Ground'), - ('PurolatorGround9AM', 'Purolator Ground 9AM'), - ('PurolatorGround10:30AM', 'Purolator Ground 10:30AM'), - ('PurolatorGroundEvening', 'Purolator Ground Evening'), - ('PurolatorQuickShip', 'Purolator Quick Ship'), - ('PurolatorQuickShipEnvelope', 'Purolator Quick Ship Envelope'), - ('PurolatorQuickShipPack', 'Purolator Quick Ship Pack'), - ('PurolatorQuickShipBox', 'Purolator Quick Ship Box'), - # 2022-09-21 - US Methods are known to rate, but cannot ship without additional customs/documents - # ('PurolatorExpressU.S.', 'Purolator Express U.S.'), - # ('PurolatorExpressU.S.9AM', 'Purolator Express U.S. 9AM'), - # ('PurolatorExpressU.S.10:30AM', 'Purolator Express U.S. 10:30AM'), - # ('PurolatorExpressU.S.12:00', 'Purolator Express U.S. 12:00'), - # ('PurolatorExpressEnvelopeU.S.', 'Purolator Express Envelope U.S.'), - # ('PurolatorExpressU.S.Envelope9AM', 'Purolator Express U.S. Envelope 9AM'), - # ('PurolatorExpressU.S.Envelope10:30AM', 'Purolator Express U.S. Envelope 10:30AM'), - # ('PurolatorExpressU.S.Envelope12:00', 'Purolator Express U.S. Envelope 12:00'), - # ('PurolatorExpressPackU.S.', 'Purolator Express Pack U.S.'), - # ('PurolatorExpressU.S.Pack9AM', 'Purolator Express U.S. Pack 9AM'), - # ('PurolatorExpressU.S.Pack10:30AM', 'Purolator Express U.S. Pack 10:30AM'), - # ('PurolatorExpressU.S.Pack12:00', 'Purolator Express U.S. Pack 12:00'), - # ('PurolatorExpressBoxU.S.', 'Purolator Express Box U.S.'), - # ('PurolatorExpressU.S.Box9AM', 'Purolator Express U.S. Box 9AM'), - # ('PurolatorExpressU.S.Box10:30AM', 'Purolator Express U.S. Box 10:30AM'), - # ('PurolatorExpressU.S.Box12:00', 'Purolator Express U.S. Box 12:00'), - # ('PurolatorGroundU.S.', 'Purolator Ground U.S.'), - # 2022-09-21 - International Methods are known to rate - # ('PurolatorExpressInternational', 'Purolator Express International'), - # ('PurolatorExpressInternational9AM', 'Purolator Express International 9AM'), - # ('PurolatorExpressInternational10:30AM', 'Purolator Express International 10:30AM'), - # ('PurolatorExpressInternational12:00', 'Purolator Express International 12:00'), - # ('PurolatorExpressEnvelopeInternational', 'Purolator Express Envelope International'), - # ('PurolatorExpressInternationalEnvelope9AM', 'Purolator Express International Envelope 9AM'), - # ('PurolatorExpressInternationalEnvelope10:30AM', 'Purolator Express International Envelope 10:30AM'), - # ('PurolatorExpressInternationalEnvelope12:00', 'Purolator Express International Envelope 12:00'), - # ('PurolatorExpressPackInternational', 'Purolator Express Pack International'), - # ('PurolatorExpressInternationalPack9AM', 'Purolator Express International Pack 9AM'), - # ('PurolatorExpressInternationalPack10:30AM', 'Purolator Express International Pack 10:30AM'), - # ('PurolatorExpressInternationalPack12:00', 'Purolator Express International Pack 12:00'), - # ('PurolatorExpressBoxInternational', 'Purolator Express Box International'), - # ('PurolatorExpressInternationalBox9AM', 'Purolator Express International Box 9AM'), - # ('PurolatorExpressInternationalBox10:30AM', 'Purolator Express International Box 10:30AM'), - # ('PurolatorExpressInternationalBox12:00', 'Purolator Express International Box 12:00'), -] - - -class ProviderPurolator(models.Model): - _inherit = 'delivery.carrier' - - delivery_type = fields.Selection(selection_add=[('purolator', 'Purolator')], - ondelete={'purolator': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0})}) - purolator_api_key = fields.Char(string='Purolator API Key', groups='base.group_system') - purolator_password = fields.Char(string='Purolator Password', groups='base.group_system') - purolator_activation_key = fields.Char(string='Purolator Activation Key', groups='base.group_system') - purolator_account_number = fields.Char(string='Purolator Account Number', groups='base.group_system') - purolator_service_type = fields.Selection(selection=PUROLATOR_SERVICES, - default='PurolatorGround') - purolator_default_package_type_id = fields.Many2one('stock.package.type', string="Purolator Package Type") - purolator_label_file_type = fields.Selection([ - ('PDF', 'PDF'), - ('ZPL', 'ZPL'), - ], default='ZPL', string="Purolator Label File Type") - - def purolator_convert_weight(self, weight): - weight_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) - - def purolator_convert_length(self, length): - 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, 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': res.get('success', True), - 'price': res.get('price', 0.0), - 'error_message': res.get('error_message', False), - 'warning_message': res.get('warning_message', False), - } - return res - 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_multi(self, order=None, picking=None, packages=None): - if not packages: - return self._purolator_rate_shipment_multi_package(order=order, picking=picking) - else: - rates = [] - for package in packages: - 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 = ['%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') % ('\n\n'.join(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_shipment_fill_options(self, request, picking=None, order=None, packages=None): - # Signature can come from any package/packages - require_signature = False - if packages: - # if ANY package has it - require_signature = any(packages.mapped('require_signature')) - else: - require_signature = self.get_signature_required(order=order, picking=picking) - # when we support international, there is also ResidentialSignatureIntl (and AdultSignatureRequired) - request.ResidentialSignatureDomestic = 'true' if require_signature else 'false' - - declared_value = 0.0 - if packages: - declared_value = sum(s or 0.0 for s in packages.mapped('declared_value')) - else: - declared_value = self.get_insurance_value(picking=picking, order=order) - if declared_value: - request.DeclaredValue = str(round(declared_value, 2)) - - request.DeclaredValue = str(self.get_insurance_value()) - # _logger.info(' _purolator_shipment_fill_options set sig.req. %s set declared val. %s' % (require_signature, declared_value)) - - 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) - - date_planned = fields.Datetime.now() - if self.env.context.get('date_planned'): - date_planned = self.env.context.get('date_planned') - - # 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) - if hasattr(date_planned, 'date'): - shipment.ShipmentDate = str(date_planned.date()) - - # populate origin information - self._purolator_fill_address(shipment.SenderInformation.Address, sender) - # populate destination - self._purolator_fill_address(shipment.ReceiverInformation.Address, receiver) - - if order: - service.estimate_shipment_add_sale_order_packages(shipment, self, order) - else: - service.estimate_shipment_add_picking_packages(shipment, self, picking, package) - - self._purolator_shipment_fill_payor(shipment, order=order, picking=picking) - self._purolator_shipment_fill_options(shipment, order=order, picking=picking, packages=package) - - 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': '\n'.join(errors), - 'warning_message': False, - }] - rates = [] - for shipment in shipment_res.ShipmentEstimates.ShipmentEstimate: - carrier = self.purolator_find_delivery_carrier_for_service(shipment['ServiceID']) - if carrier: - price = shipment['TotalPrice'] - rates.append({ - 'carrier': carrier, - 'package': package or self.env['stock.quant.package'].browse(), - 'success': True, - 'price': price if not third_party else 0.0, - 'error_message': False, - 'warning_message': _('TotalCharge not found.') if price == 0.0 else False, - 'date_planned': date_planned, - 'date_delivered': fields.Datetime.to_datetime(shipment['ExpectedDeliveryDate']), - 'transit_days': shipment['EstimatedTransitDays'], - 'service_code': shipment['ServiceID'], - }) - - return rates - - def purolator_find_delivery_carrier_for_service(self, service_code): - if self.purolator_service_type == service_code: - return self - carrier = self.search([('delivery_type', '=', 'purolator'), - ('purolator_service_type', '=', service_code), - ('purolator_account_number', '=', self.purolator_account_number), - ], limit=1) - return carrier - - def purolator_third_party(self, order=None, picking=None): - third_party_account = self.get_third_party_account(order=order, picking=picking) - if third_party_account: - if not third_party_account.delivery_type == 'purolator': - raise ValidationError('Non-Purolator Shipping Account indicated during Purolator shipment.') - return third_party_account.name - return False - - def _purolator_service(self): - return PurolatorClient( - self.purolator_api_key, - self.purolator_password, - self.purolator_activation_key, - self.purolator_account_number, - self.prod_environment, - ) - - def _purolator_address_street(self, partner): - # assume we don't have base_address_extended - street = partner.street or '' - street_pieces = [t.strip() for t in street.split(' ')] - len_street_pieces = len(street_pieces) - if len_street_pieces >= 3: - street_num = street_pieces[0] - street_type = street_pieces[2] - # TODO santize the types? I see an example for "Douglas Road" that sends "Street" - return street_num, ' '.join(street_pieces[1:]), 'Street' - elif len_street_pieces == 2: - return street_pieces[0], street_pieces[1], 'Street' - return '', street, 'Street' - - def _purolator_address_phonenumber(self, partner): - # TODO parse out of partner.phone or one of the many other phone numbers - return '1', '905', '5555555' - - - def _purolator_fill_address(self, addr, partner): - # known to not work without a name - addr.Name = partner.name - addr.Company = partner.name if partner.is_company else (partner.company_name or '') - addr.Department = '' - addr.StreetNumber, addr.StreetName, addr.StreetType = self._purolator_address_street(partner) - # addr.City = partner.city.upper() if partner.city else '' - addr.City = partner.city or '' - addr.Province = partner.state_id.code - addr.Country = partner.country_id.code - addr.PostalCode = partner.zip - addr.PhoneNumber.CountryCode, addr.PhoneNumber.AreaCode, addr.PhoneNumber.Phone = self._purolator_address_phonenumber(partner) - - def _purolator_extract_doc_blobs(self, documents_result): - res = [] - for d in getattr(documents_result.Documents, 'Document', []): - for d2 in getattr(d.DocumentDetails, 'DocumentDetail', []): - res.append(d2.Data) - return res - - # Picking Shipping - def purolator_send_shipping(self, pickings): - res = [] - service = self._purolator_service() - - for picking in pickings: - picking_packages = self.get_to_ship_picking_packages(picking) - if picking_packages is None: - continue - - shipment = service.shipment_request() - - # populate origin information - sender = self.get_shipper_warehouse(picking=picking) - self._purolator_fill_address(shipment.SenderInformation.Address, sender) - - receiver = self.get_recipient(picking=picking) - self._purolator_fill_address(shipment.ReceiverInformation.Address, receiver) - - service.shipment_add_picking_packages(shipment, self, picking, picking_packages) - - self._purolator_shipment_fill_payor(shipment, picking=picking) - self._purolator_shipment_fill_options(shipment, picking=picking, packages=picking_packages) - - 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)) - - # 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 - if picking_packages and getattr(shipment_res, 'PiecePINs', None): - piece_pins = shipment_res.PiecePINs.PIN - for p, pin in zip(picking_packages, piece_pins): - pin = pin.Value - p.carrier_tracking_ref = pin - doc_res = service.document_by_pin(pin, output_type=self.purolator_label_file_type) - for n, blob in enumerate(self._purolator_extract_doc_blobs(doc_res), 1): - document_blobs.append(('PuroPackage-%s-%s.%s' % (pin, n, self.purolator_label_file_type), b64decode(blob))) - else: - # retrieve shipment_pin document(s) - doc_res = service.document_by_pin(shipment_pin, output_type=self.purolator_label_file_type) - # _logger.info('purolator service.document_by_pin for pin %s resulted in %s' % (shipment_pin, doc_res)) - for n, blob in enumerate(self._purolator_extract_doc_blobs(doc_res), 1): - document_blobs.append(('PuroShipment-%s-%s.%s' % (shipment_pin, n, self.purolator_label_file_type), b64decode(blob))) - - if document_blobs: - logmessage = _("Shipment created in Purolator
Tracking Number/PIN : %s") % (shipment_pin) - picking.message_post(body=logmessage, attachments=document_blobs) - - picking.carrier_tracking_ref = shipment_pin - shipping_data = { - 'exact_price': picking.carrier_price, # price is set during planning - 'tracking_number': shipment_pin, - } - res.append(shipping_data) - - return res - - def purolator_get_tracking_link(self, pickings): - res = [] - for picking in pickings: - ref = picking.carrier_tracking_ref - res = res + ['https://www.purolator.com/en/shipping/tracker?pins=%s' % ref] - return res - - def purolator_cancel_shipment(self, picking, packages=None): - service = self._purolator_service() - if packages: - for package in packages: - tracking_pin = package.carrier_tracking_ref - void_res = service.shipment_void(tracking_pin) - self._purolator_format_errors(void_res, raise_class=UserError) - package.write({'carrier_tracking_ref': ''}) - picking.message_post(body=_('Package N° %s has been cancelled' % tracking_pin)) - else: - tracking_pin = picking.carrier_tracking_ref - void_res = service.shipment_void(tracking_pin) - self._purolator_format_errors(void_res, raise_class=UserError) - picking.message_post(body=_('Shipment N° %s has been cancelled' % tracking_pin)) - picking.write({'carrier_tracking_ref': '', - 'carrier_price': 0.0}) diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py deleted file mode 100644 index 9e276c73..00000000 --- a/delivery_purolator/models/purolator_services.py +++ /dev/null @@ -1,323 +0,0 @@ -from math import ceil -from requests import Session -from requests.auth import HTTPBasicAuth -from zeep import Client -from zeep.cache import SqliteCache -from zeep.transports import Transport -from odoo.exceptions import UserError - - -PUROLATOR_PIECE_SPECIAL_HANDLING_TYPE = [ - # 'AdditionalHandling', # unknown if this is "SpecialHandling" - 'FlatPackage', - 'LargePackage', - # 'Oversized', # unknown if this is "SpecialHandling" - # 'ResidentialAreaHeavyweight', # unknown if this is "SpecialHandling" -] - - -class PurolatorClient(object): - - # clients and factories - _estimating_client = None - @property - def estimating_client(self): - if not self._estimating_client: - self._estimating_client = self._get_client('/EWS/V2/Estimating/EstimatingService.asmx?wsdl', - request_reference='Rating') - return self._estimating_client - - _estimating_factory = None - @property - def estimating_factory(self): - if not self._estimating_factory: - self._estimating_factory = self.estimating_client.type_factory('ns1') - return self._estimating_factory - - _shipping_client = None - @property - def shipping_client(self): - if not self._shipping_client: - self._shipping_client = self._get_client('/EWS/V2/Shipping/ShippingService.asmx?wsdl', - request_reference='Shipping') - return self._shipping_client - - _shipping_factory = None - @property - def shipping_factory(self): - if not self._shipping_factory: - self._shipping_factory = self.shipping_client.type_factory('ns1') - return self._shipping_factory - - _shipping_documents_client = None - @property - def shipping_documents_client(self): - if not self._shipping_documents_client: - self._shipping_documents_client = self._get_client('/PWS/V1/ShippingDocuments/ShippingDocumentsService.asmx?wsdl', - version='1.3', - request_reference='ShippingDocuments') - return self._shipping_documents_client - - _shipping_documents_factory = None - @property - def shipping_documents_factory(self): - if not self._shipping_documents_factory: - self._shipping_documents_factory = self.shipping_documents_client.type_factory('ns1') - return self._shipping_documents_factory - - def __init__(self, api_key, password, activation_key, account_number, is_prod): - self.api_key = api_key - self.password = password - self.activation_key = activation_key - self.account_number = account_number - self._wsdl_base = "https://devwebservices.purolator.com" - if is_prod: - self._wsdl_base = "https://webservices.purolator.com" - - session = Session() - session.auth = HTTPBasicAuth(self.api_key, self.password) - self.transport = Transport(cache=SqliteCache(), session=session) - - def _get_client(self, wsdl_path, version='2.0', request_reference='RatingExample'): - # version added because shipping documents needs a different one - client = Client(self._wsdl_base + wsdl_path, - transport=self.transport) - request_context = client.get_element('ns1:RequestContext') - header_value = request_context( - Version=version, - Language='en', - GroupID='xxx', # TODO should we have a GroupID? - RequestReference=request_reference, - UserToken=self.activation_key, - ) - client.set_default_soapheaders([header_value]) - 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 - - :param sender_postal_code: string - :param receiver_address: dict {'City': string, - 'Province': string, - 'Country': string, - 'PostalCode': string} - :param package_type: string - :param total_weight: float (in pounds) - :returns: dict {'shipments': list, 'error': string or False} - """ - response = self.estimating_client.service.GetQuickEstimate( - BillingAccountNumber=self.account_number, - SenderPostalCode=sender_postal_code, - ReceiverAddress=receiver_address, - PackageType=package_type, - TotalWeight={ - 'Value': total_weight, - 'WeightUnit': 'lb', - }, - ) - errors = response['body']['ResponseInformation']['Errors'] - if errors: - return { - 'shipments': False, - 'error': '\n'.join(['%s: %s' % (error['Code'], error['Description']) for error in errors['Error']]), - } - shipments = response['body']['ShipmentEstimates']['ShipmentEstimate'] - if shipments: - return { - 'shipments': shipments, - 'error': False, - } - return { - 'shipments': False, - 'error': 'Purolator service did not return any matching rates.', - } - - def shipment_request(self): - 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 _add_piece_code(self, factory, piece, code): - # note that we ONLY support special handling type - if not piece.Options: - piece.Options = factory.ArrayOfOptionIDValuePair() - piece.Options.OptionIDValuePair.append(factory.OptionIDValuePair( - ID='SpecialHandling', - Value='true', - )) - piece.Options.OptionIDValuePair.append(factory.OptionIDValuePair( - ID='SpecialHandlingType', - Value=code, - )) - - 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) - total_pieces = carrier.get_package_count_for_order(order, package_type) - - package_type_codes = [t.strip() for t in (package_type.shipper_package_code or '').split(',') if t.strip() in PUROLATOR_PIECE_SPECIAL_HANDLING_TYPE] - shipment.PackageInformation.ServiceID = carrier.purolator_service_type - total_weight_value = carrier.purolator_convert_weight(order._get_estimated_weight()) - package_weight = total_weight_value / total_pieces - if total_weight_value < 1.0: - total_weight_value = 1.0 - if package_weight < 1.0: - package_weight = 1.0 - - 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', - }, - ) - for package_code in package_type_codes: - self._add_piece_code(self.estimating_factory, p, package_code) - - shipment.PackageInformation.PiecesInformation.Piece.append(p) - shipment.PackageInformation.TotalWeight.Value = str(total_weight_value) - 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 []) or 1 - if not packages: - # setup default package - package_weight = carrier.purolator_convert_weight(picking.shipping_weight) - if package_weight < 1.0: - package_weight = 1.0 - total_weight_value += package_weight - package_type = carrier.purolator_default_package_type_id - package_type_codes = [t.strip() for t in (package_type.shipper_package_code or '').split(',') if t.strip() in PUROLATOR_PIECE_SPECIAL_HANDLING_TYPE] - p = 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', - }, - ) - for package_code in package_type_codes: - self._add_piece_code(factory, p, package_code) - - shipment.PackageInformation.PiecesInformation.Piece.append(p) - else: - for package in packages: - package_weight = carrier.purolator_convert_weight(package.shipping_weight) - if package_weight < 1.0: - package_weight = 1.0 - package_type = package.package_type_id - package_type_code = package_type.shipper_package_code or '' - if package_type.package_carrier_type != 'purolator': - package_type_code = carrier.purolator_default_package_type_id.shipper_package_code or '' - package_type_codes = [t.strip() for t in package_type_code.split(',') if t.strip() in PUROLATOR_PIECE_SPECIAL_HANDLING_TYPE] - - total_weight_value += package_weight - p = 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', - }, - ) - for package_code in package_type_codes: - self._add_piece_code(factory, p, package_code) - - shipment.PackageInformation.PiecesInformation.Piece.append(p) - - shipment.PackageInformation.TotalWeight.Value = str(total_weight_value) - shipment.PackageInformation.TotalWeight.WeightUnit = 'lb' - shipment.PackageInformation.TotalPieces = str(total_pieces) - - def shipment_create(self, shipment, printer_type='Thermal'): - response = self.shipping_client.service.CreateShipment( - Shipment=shipment, - PrinterType=printer_type, - ) - return response.body - - def shipment_void(self, pin): - response = self.shipping_client.service.VoidShipment( - PIN={'Value': pin} - ) - return response.body - - def document_by_pin(self, pin, document_type='', output_type='ZPL'): - # TODO document_type? - document_criterium = self.shipping_documents_factory.ArrayOfDocumentCriteria() - document_criterium.DocumentCriteria.append(self.shipping_documents_factory.DocumentCriteria( - PIN=pin, - )) - response = self.shipping_documents_client.service.GetDocuments( - DocumentCriterium=document_criterium, - OutputType=output_type, - Synchronous=True, - ) - return response.body diff --git a/delivery_purolator/models/stock_package_type.py b/delivery_purolator/models/stock_package_type.py deleted file mode 100644 index 868e16fc..00000000 --- a/delivery_purolator/models/stock_package_type.py +++ /dev/null @@ -1,7 +0,0 @@ -from odoo import fields, models - - -class PackageType(models.Model): - _inherit = 'stock.package.type' - - package_carrier_type = fields.Selection(selection_add=[('purolator', 'Purolator')]) diff --git a/delivery_purolator/tests/__init__.py b/delivery_purolator/tests/__init__.py deleted file mode 100644 index e35bb8ef..00000000 --- a/delivery_purolator/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_purolator diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py deleted file mode 100644 index f70295fa..00000000 --- a/delivery_purolator/tests/test_purolator.py +++ /dev/null @@ -1,147 +0,0 @@ - -from odoo.tests.common import Form, TransactionCase -from odoo.exceptions import UserError - - -class TestPurolator(TransactionCase): - def setUp(self): - super().setUp() - self.carrier = self.env.ref('delivery_purolator.purolator_ground', raise_if_not_found=False) - if not self.carrier or not self.carrier.purolator_api_key: - self.skipTest('Purolator Shipping not configured, skipping tests.') - if self.carrier.prod_environment: - self.skipTest('Purolator Shipping configured to use production credentials, skipping tests.') - - # the setup for these addresses is important as there is - # error handling on purolator's side - self.state_ca_ontario = self.env.ref('base.state_ca_on') - self.country_ca = self.state_ca_ontario.country_id - - self.shipper_partner = self.env['res.partner'].create({ - 'name': 'The Great North Ltd.', - 'zip': 'L4W5M8', - 'street': '1234 Test St.', - 'state_id': self.state_ca_ontario.id, - 'country_id': self.country_ca.id, - 'city': 'Mississauga', # note other city will return error for this field+zip - }) - self.shipper_warehouse = self.env['stock.warehouse'].create({ - 'partner_id': self.shipper_partner.id, - 'name': 'Canadian Warehouse', - 'code': 'CWH', - }) - self.receiver_partner = self.env['res.partner'].create({ - 'name': 'Receiver Address', - 'city': 'Burnaby', - 'street': '1234 Test Rd.', - 'state_id': self.ref('base.state_ca_bc'), - 'country_id': self.ref('base.ca'), - 'zip': 'V5C5A9', - }) - self.storage_box = self.env.ref('product.product_product_6') - self.storage_box.weight = 1.5 # Something more reasonable - # Make some available - self.env['stock.quant']._update_available_quantity(self.storage_box, self.shipper_warehouse.lot_stock_id, 100) - self.sale_order = self.env['sale.order'].create({ - 'partner_id': self.receiver_partner.id, - 'warehouse_id': self.shipper_warehouse.id, - 'order_line': [(0, 0, { - 'name': self.storage_box.name, - 'product_id': self.storage_box.id, - 'product_uom_qty': 3.0, - 'product_uom': self.storage_box.uom_id.id, - 'price_unit': self.storage_box.lst_price, - })], - }) - - # reconfigure this method so that we can set its default package to one that needs a service code - self.delivery_carrier_ground = self.env.ref('delivery_purolator.purolator_ground') - self.delivery_carrier_ground.purolator_default_package_type_id = self.env.ref('delivery_purolator.purolator_packaging_large_package') - # set a VERY low requirement for signature - self.delivery_carrier_ground.automatic_insurance_value = 0.1 - self.delivery_carrier_ground.automatic_sig_req_value = 0.1 - - def _so_pick_shipping(self): - # Regular Update Shipping functionality - delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({ - 'default_order_id': self.sale_order.id, - 'default_carrier_id': self.ref('delivery_purolator.purolator_ground'), - })) - choose_delivery_carrier = delivery_wizard.save() - choose_delivery_carrier.update_price() - self.assertGreater(choose_delivery_carrier.delivery_price, 0.0, "Purolator delivery cost for this SO has not been correctly estimated.") - choose_delivery_carrier.button_confirm() - self.assertEqual(self.sale_order.carrier_id, self.carrier) - - def test_00_rate_order(self): - self._so_pick_shipping() - - # Multi-rating with sale order - rates = self.carrier.rate_shipment_multi(order=self.sale_order) - carrier_express = self.env.ref('delivery_purolator.purolator_express') - rate_express = list(filter(lambda r: r['carrier'] == carrier_express, rates)) - rate_express = rate_express and rate_express[0] - self.assertFalse(rate_express['error_message']) - self.assertGreater(rate_express['price'], 0.0) - self.assertGreater(rate_express['transit_days'], 0) - self.assertEqual(rate_express['package'], self.env['stock.quant.package'].browse()) - - # Multi-rating with picking - self.sale_order.action_confirm() - picking = self.sale_order.picking_ids - self.assertEqual(len(picking), 1) - rates = self.carrier.rate_shipment_multi(picking=picking) - rate_express = list(filter(lambda r: r['carrier'] == carrier_express, rates)) - rate_express = rate_express and rate_express[0] - self.assertFalse(rate_express['error_message']) - self.assertGreater(rate_express['price'], 0.0) - self.assertGreater(rate_express['transit_days'], 0) - self.assertEqual(rate_express['package'], self.env['stock.quant.package'].browse()) - - # Multi-rate package - self.assertEqual(picking.move_lines.reserved_availability, 3.0) - picking.move_line_ids.qty_done = 1.0 - context = dict( - current_package_carrier_type=picking.carrier_id.delivery_type, - default_picking_id=picking.id - ) - choose_package_wizard = self.env['choose.delivery.package'].with_context(context).create({}) - self.assertEqual(choose_package_wizard.shipping_weight, 1.5) - choose_package_wizard.action_put_in_pack() - package = picking.move_line_ids.mapped('result_package_id') - self.assertEqual(len(package), 1) - - rates = self.carrier.rate_shipment_multi(picking=picking, packages=package) - rate_express = list(filter(lambda r: r['carrier'] == carrier_express, rates)) - rate_express = rate_express and rate_express[0] - self.assertFalse(rate_express['error_message']) - self.assertGreater(rate_express['price'], 0.0) - self.assertGreater(rate_express['transit_days'], 0) - self.assertEqual(rate_express['package'], package) - - def test_20_shipping(self): - self._so_pick_shipping() - self.sale_order.action_confirm() - picking = self.sale_order.picking_ids - self.assertEqual(picking.carrier_id, self.carrier) - self.assertEqual(picking.message_attachment_count, 0) - - # Test Error handling: - # Not having a city will result in an error - original_shipper_partner_city = self.shipper_partner.city - self.shipper_partner.city = '' - with self.assertRaises(UserError): - picking.send_to_shipper() - self.shipper_partner.city = original_shipper_partner_city - - # Basic case: no qty done or packages or anything at all really - # it makes sense to be able to do 'something' in this case - picking.carrier_price = 50.0 - picking.send_to_shipper() - self.assertTrue(picking.carrier_tracking_ref) - self.assertEqual(picking.message_attachment_count, 1) # has tracking label now - self.assertEqual(picking.carrier_price, 50.0) # price is set during planning and should remain unchanged - - # Void - picking.cancel_shipment() - self.assertFalse(picking.carrier_tracking_ref) diff --git a/delivery_purolator/views/delivery_purolator_views.xml b/delivery_purolator/views/delivery_purolator_views.xml deleted file mode 100644 index 413823e0..00000000 --- a/delivery_purolator/views/delivery_purolator_views.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - delivery.carrier.form.provider.purolator - delivery.carrier - - - - - - - - - - - - - - - - - - - - - -