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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-