From 33eaa2e7b9c40add1a5eee225d9a3c321f57da31 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 7 Sep 2022 16:36:13 +0000 Subject: [PATCH 01/25] [IMP] delivery_hibou: weight or volumetric package selection --- delivery_hibou/models/delivery.py | 33 ++++++++++++- delivery_hibou/models/stock.py | 12 +++++ delivery_hibou/tests/test_delivery_hibou.py | 55 +++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index c604c02a..fd6fb9e7 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -17,9 +17,40 @@ class DeliveryCarrier(models.Model): string='Procurement Priority', help='Priority for this carrier. Will affect pickings ' 'and procurements related to this carrier.') + package_by_field = fields.Selection([ + ('', 'Use Default Package Type'), + ('weight', 'Weight'), + ('volume', 'Volume'), + ], string='Packaging by Product Field') + + # Package selection + def get_package_type_for_order(self, order): + if self.package_by_field == 'weight': + return self._get_package_type_for_order(order, 'max_weight', 'weight') + elif self.package_by_field == 'volume': + return self._get_package_type_for_order(order, 'package_volume', 'volume') + attr = getattr(self, '%s_default_packaging_id' % (self.delivery_type, ), None) + if attr: + return attr() + attr = getattr(self, '%s_default_package_type_id' % (self.delivery_type, ), None) + if attr: + return attr() + return self.env['stock.package.type'] + + def _get_package_type_for_order(self, order, package_type_field, product_field): + order_total = sum(order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')).mapped(lambda ol: ol.product_id[product_field] * ol.product_uom_qty)) + if order_total: + package_types = self.env['stock.package.type'].search([ + ('package_carrier_type', 'in', ('none', False, self.delivery_type)), + ('use_in_package_selection', '=', True), + ], order=package_type_field) + package_type = None + for package_type in package_types: + if package_type[package_type_field] >= order_total: + return package_type + return package_types if not package_type else package_type # Utility - def get_insurance_value(self, order=None, picking=None, package=None): value = 0.0 if order: diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 050a7f03..b045dc4b 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -2,6 +2,18 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError +class StockPackageType(models.Model): + _inherit = 'stock.package.type' + + use_in_package_selection = fields.Boolean() + package_volume = fields.Float(compute='_compute_package_volume', store=True) + + @api.depends('packaging_length', 'width', 'height') + def _compute_package_volume(self): + for pt in self: + pt.package_volume = pt.packaging_length * pt.width * pt.height + + class StockQuantPackage(models.Model): _inherit = 'stock.quant.package' diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index 1dd9aa99..43d1de71 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -7,6 +7,11 @@ class TestDeliveryHibou(common.TransactionCase): super(TestDeliveryHibou, self).setUp() self.partner = self.env.ref('base.res_partner_address_13') self.product = self.env.ref('product.product_product_7') + self.product.write({ + 'type': 'product', + 'weight': 1.0, + 'volume': 15.0, + }) # Create Shipping Account self.shipping_account = self.env['partner.shipping.account'].create({ 'name': '123123', @@ -20,6 +25,26 @@ class TestDeliveryHibou(common.TransactionCase): self.carrier = self.env['delivery.carrier'].create({ 'name': 'Test Carrier1', 'product_id': self.delivery_product.id, + 'delivery_type': 'fixed', + }) + # update all other package types to have + self.package_type_large = self.env['stock.package.type'].create({ + 'name': 'Large 15x15x15', + 'packaging_length': 15.0, + 'height': 15.0, + 'width': 15.0, + 'max_weight': 50.0, + 'package_carrier_type': 'none', + 'use_in_package_selection': True, + }) + self.package_type_small = self.env['stock.package.type'].create({ + 'name': 'Small 2x2x4', + 'packaging_length': 4.0, + 'height': 2.0, + 'width': 2.0, + 'max_weight': 1.0, + 'package_carrier_type': 'none', + 'use_in_package_selection': True, }) def test_delivery_hibou(self): @@ -40,6 +65,7 @@ class TestDeliveryHibou(common.TransactionCase): 'shipping_account_id': self.shipping_account.id, 'order_line': [(0, 0, { 'product_id': self.product.id, + 'product_uom_qty': 2.0, })] }) self.assertFalse(sale_order.carrier_id) @@ -62,6 +88,35 @@ class TestDeliveryHibou(common.TransactionCase): self.assertEqual(sale_order.picking_ids.shipping_account_id, self.shipping_account) self.assertEqual(sale_order.carrier_id.get_third_party_account(order=sale_order), self.shipping_account) + # Test Package selection + default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order) + self.assertFalse(default_package_type, 'Fixed should not have a default packaging type.') + + # by product weight + sale_order.carrier_id.package_by_field = 'weight' + + default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order) + self.assertTrue(default_package_type) + self.assertEqual(default_package_type, self.package_type_large) + + # change qty ordered to try to get the small package type + sale_order.order_line.write({ + 'product_uom_qty': 1.0, + }) + default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order) + self.assertEqual(default_package_type, self.package_type_small) + + # by product volume + sale_order.carrier_id.package_by_field = 'volume' + default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order) + self.assertEqual(default_package_type, self.package_type_small) + + sale_order.order_line.write({ + 'product_uom_qty': 2.0, + }) + default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order) + self.assertEqual(default_package_type, self.package_type_large) + # Test attn test_ref = 'TEST100' self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), False) From a4626f2ef0a39a0e6cf1b3bd8b77afde98747334 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Mon, 29 Aug 2022 18:50:39 -0500 Subject: [PATCH 02/25] [ADD] delivery_purolator H10820 --- delivery_purolator/__init__.py | 1 + delivery_purolator/__manifest__.py | 28 +++++++ .../data/delivery_purolator_demo.xml | 23 +++++ delivery_purolator/models/__init__.py | 1 + .../models/delivery_purolator.py | 41 +++++++++ .../models/purolator_services.py | 83 +++++++++++++++++++ delivery_purolator/tests/__init__.py | 1 + delivery_purolator/tests/test_purolator.py | 50 +++++++++++ .../views/delivery_purolator_views.xml | 25 ++++++ 9 files changed, 253 insertions(+) create mode 100644 delivery_purolator/__init__.py create mode 100644 delivery_purolator/__manifest__.py create mode 100644 delivery_purolator/data/delivery_purolator_demo.xml create mode 100644 delivery_purolator/models/__init__.py create mode 100644 delivery_purolator/models/delivery_purolator.py create mode 100644 delivery_purolator/models/purolator_services.py create mode 100644 delivery_purolator/tests/__init__.py create mode 100644 delivery_purolator/tests/test_purolator.py create mode 100644 delivery_purolator/views/delivery_purolator_views.xml diff --git a/delivery_purolator/__init__.py b/delivery_purolator/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_purolator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_purolator/__manifest__.py b/delivery_purolator/__manifest__.py new file mode 100644 index 00000000..b3c7394e --- /dev/null +++ b/delivery_purolator/__manifest__.py @@ -0,0 +1,28 @@ +{ + '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': [ + 'views/delivery_purolator_views.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/delivery_purolator/data/delivery_purolator_demo.xml b/delivery_purolator/data/delivery_purolator_demo.xml new file mode 100644 index 00000000..2b8cbcc2 --- /dev/null +++ b/delivery_purolator/data/delivery_purolator_demo.xml @@ -0,0 +1,23 @@ + + + + + + + Purolator Delivery + Delivery_Puro + service + + + + 0.0 + order + + + Purolator Test Carrier + + purolator + + + + diff --git a/delivery_purolator/models/__init__.py b/delivery_purolator/models/__init__.py new file mode 100644 index 00000000..ab7a929c --- /dev/null +++ b/delivery_purolator/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_purolator diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py new file mode 100644 index 00000000..8138c337 --- /dev/null +++ b/delivery_purolator/models/delivery_purolator.py @@ -0,0 +1,41 @@ +from odoo import fields, models, _ +from .purolator_services import PurolatorClient + + +class ProviderPurolator(models.Model): + _inherit = 'delivery.carrier' + + delivery_type = fields.Selection(selection_add=[('purolator', 'Purolator')], ondelete={'purolator': 'cascade'}) + 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([('PurolatorExpress', 'PurolatorExpress')], default='PurolatorExpress') + + def purolator_rate_shipment(self, order): + # sudoself = self.sudo() + 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, + } + weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() + weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) + client = PurolatorClient(self) + res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) + if res['error']: + return { + 'success': False, + 'price': 0.0, + 'error_message': _(res['error']), + 'warning_message': False, + } + return { + 'success': True, + 'price': res['price'], + 'error_message': False, + 'warning_message': False, + } diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py new file mode 100644 index 00000000..0fc09c33 --- /dev/null +++ b/delivery_purolator/models/purolator_services.py @@ -0,0 +1,83 @@ +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 +import logging +_logger = logging.getLogger(__name__) + + +class PurolatorClient(object): + def __init__(self, carrier): + if carrier.delivery_type != 'purolator': + raise UserError('Invalid carrier: %s' % carrier.name) + self.api_key = carrier.purolator_api_key + self.password = carrier.purolator_password + self.activation_key = carrier.purolator_activation_key + self.account_number = carrier.purolator_account_number + self.service_type = carrier.purolator_service_type + self._wsdl_base = "https://devwebservices.purolator.com" + if carrier.prod_environment: + 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): + client = Client(self._wsdl_base + wsdl_path, + transport=self.transport) + request_context = client.get_element('ns1:RequestContext') + header_value = request_context( + Version='2.0', + Language='en', + GroupID='xxx', + RequestReference='RatingExample', + UserToken=self.activation_key, + ) + # _logger.warning('*** header_value:\n%s' % header_value) + client.set_default_soapheaders([header_value]) + return client + + 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 + """ + client = self._get_client('/EWS/V2/Estimating/EstimatingService.asmx?wsdl') + response = client.service.GetQuickEstimate( + BillingAccountNumber=self.account_number, + SenderPostalCode=sender_postal_code, + ReceiverAddress=receiver_address, + PackageType=package_type, + TotalWeight={ + 'Value': 10.0, + 'WeightUnit': 'lb', + }, + ) + # _logger.warning('**** GetQuickEstimate response:\n%s', response) + errors = response['body']['ResponseInformation']['Errors'] + if errors: + return { + 'price': 0.0, + 'error': '\n'.join(['%s: %s' % (error['Code'], error['Description']) for error in errors['Error']]), + } + shipments = response['body']['ShipmentEstimates']['ShipmentEstimate'] + shipment = list(filter(lambda s: s['ServiceID'] == self.service_type, shipments)) + if shipment: + return { + 'price': shipment[0]['TotalPrice'], + 'error': False, + } + return { + 'price': 0.0, + 'error': 'Purolator ServiceID not found', + } diff --git a/delivery_purolator/tests/__init__.py b/delivery_purolator/tests/__init__.py new file mode 100644 index 00000000..e35bb8ef --- /dev/null +++ b/delivery_purolator/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purolator diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py new file mode 100644 index 00000000..9e4e546e --- /dev/null +++ b/delivery_purolator/tests/test_purolator.py @@ -0,0 +1,50 @@ + +from odoo.tests.common import Form, TransactionCase + + +class TestPurolator(TransactionCase): + def setUp(self): + super().setUp() + self.carrier = self.env.ref('delivery_purolator.purolator_carrier', 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.') + + self.shipper_partner = self.env['res.partner'].create({ + 'name': 'Canadian Address', + 'zip': 'L4W5M8', + }) + 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', + '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.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, + })], + }) + + def test_00_rate_order(self): + 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_carrier'), + })) + 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.") diff --git a/delivery_purolator/views/delivery_purolator_views.xml b/delivery_purolator/views/delivery_purolator_views.xml new file mode 100644 index 00000000..fd57bea5 --- /dev/null +++ b/delivery_purolator/views/delivery_purolator_views.xml @@ -0,0 +1,25 @@ + + + + + delivery.carrier.form.provider.purolator + delivery.carrier + + + + + + + + + + + + + + + + + + + From b5c6f8997037f733f511eccf4ccf6013862f6c27 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Wed, 31 Aug 2022 17:37:10 -0500 Subject: [PATCH 03/25] [IMP] delivery_purolator: add service types and decouple api from `delivery.carrier` H10820 --- .../models/delivery_purolator.py | 89 ++++++++++++++++++- .../models/purolator_services.py | 28 +++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 8138c337..00d18c03 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -2,15 +2,82 @@ from odoo import fields, models, _ from .purolator_services import PurolatorClient +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'), + ('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.'), + ('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': 'cascade'}) + 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([('PurolatorExpress', 'PurolatorExpress')], default='PurolatorExpress') + purolator_service_type = fields.Selection(selection=PUROLATOR_SERVICES, + default='PurolatorGround') def purolator_rate_shipment(self, order): # sudoself = self.sudo() @@ -24,7 +91,13 @@ class ProviderPurolator(models.Model): } weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) - client = PurolatorClient(self) + client = PurolatorClient( + self.purolator_api_key, + self.purolator_password, + self.purolator_activation_key, + self.purolator_account_number, + self.prod_environment, + ) res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) if res['error']: return { @@ -33,9 +106,17 @@ class ProviderPurolator(models.Model): '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, + } return { 'success': True, - 'price': res['price'], + 'price': shipment[0]['TotalPrice'], 'error_message': False, 'warning_message': False, } diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index 0fc09c33..c82db5da 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -9,16 +9,13 @@ _logger = logging.getLogger(__name__) class PurolatorClient(object): - def __init__(self, carrier): - if carrier.delivery_type != 'purolator': - raise UserError('Invalid carrier: %s' % carrier.name) - self.api_key = carrier.purolator_api_key - self.password = carrier.purolator_password - self.activation_key = carrier.purolator_activation_key - self.account_number = carrier.purolator_account_number - self.service_type = carrier.purolator_service_type + 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 carrier.prod_environment: + if is_prod: self._wsdl_base = "https://webservices.purolator.com" session = Session() @@ -50,7 +47,7 @@ class PurolatorClient(object): 'PostalCode': string} :param package_type: string :param total_weight: float (in pounds) - :returns: dict + :returns: dict {'shipments': list, 'error': string or False} """ client = self._get_client('/EWS/V2/Estimating/EstimatingService.asmx?wsdl') response = client.service.GetQuickEstimate( @@ -67,17 +64,16 @@ class PurolatorClient(object): errors = response['body']['ResponseInformation']['Errors'] if errors: return { - 'price': 0.0, + 'shipments': False, 'error': '\n'.join(['%s: %s' % (error['Code'], error['Description']) for error in errors['Error']]), } shipments = response['body']['ShipmentEstimates']['ShipmentEstimate'] - shipment = list(filter(lambda s: s['ServiceID'] == self.service_type, shipments)) - if shipment: + if shipments: return { - 'price': shipment[0]['TotalPrice'], + 'shipments': shipments, 'error': False, } return { - 'price': 0.0, - 'error': 'Purolator ServiceID not found', + 'shipments': False, + 'error': 'Purolator service did not return any matching rates.', } From d5cf40506eea55c84af1c6d565f097c23032565b Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Wed, 31 Aug 2022 23:02:37 -0500 Subject: [PATCH 04/25] [FIX] delivery_hibou: remove implicit dependency on `sale_sourced_by_line` --- delivery_hibou/models/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index fd6fb9e7..d0f9ae10 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -254,7 +254,7 @@ class DeliveryCarrier(models.Model): else: if packages: raise UserError(_('Cannot rate package without picking.')) - self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now())) + self = self.with_context(date_planned=('date_planned' in order._fields and order.date_planned or fields.Datetime.now())) res = [] for carrier in self: From b63d2b043632fbd21656b7313a71eda0ec869b6f Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Wed, 31 Aug 2022 23:44:33 -0500 Subject: [PATCH 05/25] [IMP] delivery_purolator: add multi-rating for orders H10820 --- .../data/delivery_purolator_demo.xml | 31 +++++++-- .../models/delivery_purolator.py | 65 +++++++++++++++++++ .../models/purolator_services.py | 4 -- delivery_purolator/tests/test_purolator.py | 13 +++- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/delivery_purolator/data/delivery_purolator_demo.xml b/delivery_purolator/data/delivery_purolator_demo.xml index 2b8cbcc2..a8746dc8 100644 --- a/delivery_purolator/data/delivery_purolator_demo.xml +++ b/delivery_purolator/data/delivery_purolator_demo.xml @@ -3,9 +3,10 @@ - - Purolator Delivery - Delivery_Puro + + + Purolator Express + Delivery_PurolatorExpress service @@ -13,10 +14,30 @@ 0.0 order - - Purolator Test Carrier + + Purolator Express Test purolator + PurolatorExpress + + + + + Purolator Ground + Delivery_PurolatorGround + service + + + + 0.0 + order + + + Purolator Ground Test + + purolator + PurolatorGround + diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 00d18c03..a18c63f1 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -1,5 +1,7 @@ from odoo import fields, models, _ from .purolator_services import PurolatorClient +import logging +_logger = logging.getLogger(__name__) PUROLATOR_SERVICES = [ @@ -99,6 +101,7 @@ class ProviderPurolator(models.Model): self.prod_environment, ) res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) + _logger.warning('get_quick_estimate: %s', res) if res['error']: return { 'success': False, @@ -120,3 +123,65 @@ class ProviderPurolator(models.Model): 'error_message': False, 'warning_message': False, } + + def purolator_rate_shipment_multi(self, order=None, picking=None, packages=None): + 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() + + date_planned = fields.Datetime.now() + if self.env.context.get('date_planned'): + date_planned = self.env.context.get('date_planned') + + if order: + weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) + else: + raise NotImplementedError + client = PurolatorClient( + self.purolator_api_key, + self.purolator_password, + self.purolator_activation_key, + self.purolator_account_number, + self.prod_environment, + ) + res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) + if res['error']: + return [{'carrier': self, + 'success': False, + 'price': 0.0, + 'error_message': _('Error:\n%s') % res['error'], + 'warning_message': False, + }] + rates = [] + for shipment in res['shipments']: + 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, + 'error_message': False, + 'warning_message': _('TotalCharge not found.') if price == 0.0 else False, + 'date_planned': date_planned, + 'date_delivered': fields.Date.to_date(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) + ], limit=1) + return carrier diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index c82db5da..bc69c095 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -4,8 +4,6 @@ from zeep import Client from zeep.cache import SqliteCache from zeep.transports import Transport from odoo.exceptions import UserError -import logging -_logger = logging.getLogger(__name__) class PurolatorClient(object): @@ -33,7 +31,6 @@ class PurolatorClient(object): RequestReference='RatingExample', UserToken=self.activation_key, ) - # _logger.warning('*** header_value:\n%s' % header_value) client.set_default_soapheaders([header_value]) return client @@ -60,7 +57,6 @@ class PurolatorClient(object): 'WeightUnit': 'lb', }, ) - # _logger.warning('**** GetQuickEstimate response:\n%s', response) errors = response['body']['ResponseInformation']['Errors'] if errors: return { diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index 9e4e546e..2febb852 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -5,7 +5,7 @@ from odoo.tests.common import Form, TransactionCase class TestPurolator(TransactionCase): def setUp(self): super().setUp() - self.carrier = self.env.ref('delivery_purolator.purolator_carrier', raise_if_not_found=False) + 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: @@ -41,10 +41,19 @@ class TestPurolator(TransactionCase): }) def test_00_rate_order(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_carrier'), + '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.") + + # Multi-rating with sale order + rates = self.carrier.rate_shipment_multi(order=self.sale_order) + carrier_express = self.env.ref('delivery_purolator.purolator_ground') + rate_express = list(filter(lambda r: r['carrier'] == carrier_express, rates)) + rate_express = rate_express and rate_express[0] + self.assertGreater(rate_express['price'], 0.0) + self.assertGreater(rate_express['transit_days'], 0) From 0bd0fc924e9b61eeb7bdd6bbc49f9a5970970ae1 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Thu, 1 Sep 2022 17:22:05 -0500 Subject: [PATCH 06/25] [IMP] delivery_purolator: add multi-rating for pickings and packages H10820 --- delivery_purolator/__manifest__.py | 1 + .../data/delivery_purolator_data.xml | 12 ++++++ .../data/delivery_purolator_demo.xml | 2 + delivery_purolator/models/__init__.py | 1 + .../models/delivery_purolator.py | 30 +++++++++++--- .../models/stock_package_type.py | 7 ++++ delivery_purolator/tests/test_purolator.py | 39 +++++++++++++++++++ .../views/delivery_purolator_views.xml | 1 + 8 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 delivery_purolator/data/delivery_purolator_data.xml create mode 100644 delivery_purolator/models/stock_package_type.py diff --git a/delivery_purolator/__manifest__.py b/delivery_purolator/__manifest__.py index b3c7394e..cc305e42 100644 --- a/delivery_purolator/__manifest__.py +++ b/delivery_purolator/__manifest__.py @@ -21,6 +21,7 @@ Purolator Shipping 'data/delivery_purolator_demo.xml', ], 'data': [ + 'data/delivery_purolator_data.xml', 'views/delivery_purolator_views.xml', ], 'auto_install': False, diff --git a/delivery_purolator/data/delivery_purolator_data.xml b/delivery_purolator/data/delivery_purolator_data.xml new file mode 100644 index 00000000..503d26de --- /dev/null +++ b/delivery_purolator/data/delivery_purolator_data.xml @@ -0,0 +1,12 @@ + + + + + + Purolator Customer Packaging + CustomerPackaging + purolator + + + + diff --git a/delivery_purolator/data/delivery_purolator_demo.xml b/delivery_purolator/data/delivery_purolator_demo.xml index a8746dc8..2527e7cd 100644 --- a/delivery_purolator/data/delivery_purolator_demo.xml +++ b/delivery_purolator/data/delivery_purolator_demo.xml @@ -20,6 +20,7 @@ purolator PurolatorExpress + @@ -38,6 +39,7 @@ purolator PurolatorGround + diff --git a/delivery_purolator/models/__init__.py b/delivery_purolator/models/__init__.py index ab7a929c..8a30199b 100644 --- a/delivery_purolator/models/__init__.py +++ b/delivery_purolator/models/__init__.py @@ -1 +1,2 @@ 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 index a18c63f1..f6fa16b4 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -1,7 +1,6 @@ from odoo import fields, models, _ from .purolator_services import PurolatorClient import logging -_logger = logging.getLogger(__name__) PUROLATOR_SERVICES = [ @@ -80,6 +79,7 @@ class ProviderPurolator(models.Model): 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") def purolator_rate_shipment(self, order): # sudoself = self.sudo() @@ -100,8 +100,12 @@ class ProviderPurolator(models.Model): self.purolator_account_number, self.prod_environment, ) - res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) - _logger.warning('get_quick_estimate: %s', res) + res = client.get_quick_estimate( + sender.zip, + receiver_address, + self.purolator_default_package_type_id.shipper_package_code, + weight, + ) if res['error']: return { 'success': False, @@ -125,6 +129,15 @@ class ProviderPurolator(models.Model): } 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_rate_shipment_multi_package(self, order=None, picking=None, package=None): sender = self.get_shipper_warehouse(order=order, picking=picking) receiver = self.get_recipient(order=order, picking=picking) receiver_address = { @@ -139,10 +152,15 @@ class ProviderPurolator(models.Model): if self.env.context.get('date_planned'): date_planned = self.env.context.get('date_planned') + package_code = self.purolator_default_package_type_id.shipper_package_code if order: weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) else: - raise NotImplementedError + if package: + weight = package.shipping_weight + package_code = package.package_type_id.shipper_package_code or package_code + else: + weight = picking.shipping_weight or picking.weight client = PurolatorClient( self.purolator_api_key, self.purolator_password, @@ -150,7 +168,7 @@ class ProviderPurolator(models.Model): self.purolator_account_number, self.prod_environment, ) - res = client.get_quick_estimate(sender.zip, receiver_address, 'CustomerPackaging', weight) + res = client.get_quick_estimate(sender.zip, receiver_address, package_code, weight) if res['error']: return [{'carrier': self, 'success': False, @@ -165,7 +183,7 @@ class ProviderPurolator(models.Model): price = shipment['TotalPrice'] rates.append({ 'carrier': carrier, - # 'package': package or self.env['stock.quant.package'].browse(), + 'package': package or self.env['stock.quant.package'].browse(), 'success': True, 'price': price, 'error_message': False, diff --git a/delivery_purolator/models/stock_package_type.py b/delivery_purolator/models/stock_package_type.py new file mode 100644 index 00000000..868e16fc --- /dev/null +++ b/delivery_purolator/models/stock_package_type.py @@ -0,0 +1,7 @@ +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/test_purolator.py b/delivery_purolator/tests/test_purolator.py index 2febb852..844849d2 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -28,6 +28,9 @@ class TestPurolator(TransactionCase): '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, @@ -55,5 +58,41 @@ class TestPurolator(TransactionCase): carrier_express = self.env.ref('delivery_purolator.purolator_ground') 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 + picking.carrier_id = self.carrier + 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) diff --git a/delivery_purolator/views/delivery_purolator_views.xml b/delivery_purolator/views/delivery_purolator_views.xml index fd57bea5..695f56dd 100644 --- a/delivery_purolator/views/delivery_purolator_views.xml +++ b/delivery_purolator/views/delivery_purolator_views.xml @@ -15,6 +15,7 @@ + From 22a32f958253873f84c1dda2a3e372c20917ced0 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 14 Sep 2022 00:09:13 +0000 Subject: [PATCH 07/25] [IMP] delivery_purolator: demo data and tests, method for weight --- .../data/delivery_purolator_demo.xml | 20 +++++++++---------- .../models/delivery_purolator.py | 18 ++++++++++++----- delivery_purolator/tests/test_purolator.py | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/delivery_purolator/data/delivery_purolator_demo.xml b/delivery_purolator/data/delivery_purolator_demo.xml index 2527e7cd..7388c15d 100644 --- a/delivery_purolator/data/delivery_purolator_demo.xml +++ b/delivery_purolator/data/delivery_purolator_demo.xml @@ -13,15 +13,15 @@ 0.0 order - - - Purolator Express Test - - purolator - PurolatorExpress - - - + + + Purolator Express Test + + purolator + PurolatorExpress + + + Purolator Ground @@ -35,7 +35,7 @@ Purolator Ground Test - + purolator PurolatorGround diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index f6fa16b4..4efeef97 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -81,6 +81,10 @@ class ProviderPurolator(models.Model): default='PurolatorGround') purolator_default_package_type_id = fields.Many2one('stock.package.type', string="Purolator Package Type") + def _purolator_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_rate_shipment(self, order): # sudoself = self.sudo() sender = self.get_shipper_warehouse(order=order) @@ -91,8 +95,8 @@ class ProviderPurolator(models.Model): 'Country': receiver.country_id.code, 'PostalCode': receiver.zip, } - weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() - weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) + # TODO packaging volume/length/width/height + weight = self._purolator_weight(order._get_estimated_weight()) client = PurolatorClient( self.purolator_api_key, self.purolator_password, @@ -147,20 +151,23 @@ class ProviderPurolator(models.Model): '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() if self.env.context.get('date_planned'): date_planned = self.env.context.get('date_planned') + # TODO need packaging volume/dimensions package_code = self.purolator_default_package_type_id.shipper_package_code if order: - weight = weight_uom_id._compute_quantity(order._get_estimated_weight(), self.env.ref('uom.product_uom_lb'), round=False) + weight = order._get_estimated_weight() else: if package: weight = package.shipping_weight - package_code = package.package_type_id.shipper_package_code or package_code + 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_weight(weight) client = PurolatorClient( self.purolator_api_key, self.purolator_password, @@ -200,6 +207,7 @@ class ProviderPurolator(models.Model): if self.purolator_service_type == service_code: return self carrier = self.search([('delivery_type', '=', 'purolator'), - ('purolator_service_type', '=', service_code) + ('purolator_service_type', '=', service_code), + ('purolator_account_number', '=', self.purolator_account_number), ], limit=1) return carrier diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index 844849d2..d10de4cc 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -55,7 +55,7 @@ class TestPurolator(TransactionCase): # Multi-rating with sale order rates = self.carrier.rate_shipment_multi(order=self.sale_order) - carrier_express = self.env.ref('delivery_purolator.purolator_ground') + 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']) From def7e5da740ce2deade41e7def5140d91be565b6 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 14 Sep 2022 00:41:30 +0000 Subject: [PATCH 08/25] [IMP] delivery_hibou: add get_to_ship_picking_packages utility I find that this must be in the pre-amble of iteration on pickings to ensure they have the correct output when package carriers are in play. Specifically, we need to be able to 're-enter' the send to shipper method with a new package for a different carrier and not even try to ship if all packages have tracking numbers. --- delivery_hibou/models/delivery.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index d0f9ae10..b3f928e2 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -51,6 +51,20 @@ class DeliveryCarrier(models.Model): return package_types if not package_type else package_type # Utility + def get_to_ship_picking_packages(self, picking): + # Will return a stock.quant.package record set if the picking has packages + # in the case of multi-packing and none applicable, will return None + # Additionally, will not return packages that have a tracking number (because they have shipped) + picking_packages = picking.package_ids + package_carriers = picking_packages.mapped('carrier_id') + if package_carriers: + # only ship ours + picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref) + + if package_carriers and not picking_packages: + return None + return picking_packages + def get_insurance_value(self, order=None, picking=None, package=None): value = 0.0 if order: From dc5fb86248a683ee4ce26fee86d164a6c483e506 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 14 Sep 2022 00:44:09 +0000 Subject: [PATCH 09/25] [IMP] delivery_purolator: add tests, basic send_shipping outline --- .../models/delivery_purolator.py | 23 ++++++++++++++++++ delivery_purolator/tests/test_purolator.py | 24 +++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 4efeef97..de016596 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -211,3 +211,26 @@ class ProviderPurolator(models.Model): ('purolator_account_number', '=', self.purolator_account_number), ], limit=1) return carrier + + # Picking Shipping + def purolator_send_shipping(self, pickings): + res = [] + # service = self._get_purolator_service() + # had_customs = False + + for picking in pickings: + picking_packages = self.get_to_ship_picking_packages(picking) + if picking_packages is None: + continue + + # do the shipment! + package_labels = [] + for x in []: + res = res + [shipping_data] # bug! fill in with appropriate data + picking.carrier_tracking_ref = ','.join(package_labels) + + # FIXME + shipping_data = {'exact_price': 1.0, + 'tracking_number': ''} + res.append(shipping_data) + return res diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index d10de4cc..f9157cf7 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -43,8 +43,8 @@ class TestPurolator(TransactionCase): })], }) - def test_00_rate_order(self): - # Regular Update Shipping functionality + 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'), @@ -52,7 +52,12 @@ class TestPurolator(TransactionCase): 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') @@ -76,7 +81,6 @@ class TestPurolator(TransactionCase): self.assertEqual(rate_express['package'], self.env['stock.quant.package'].browse()) # Multi-rate package - picking.carrier_id = self.carrier self.assertEqual(picking.move_lines.reserved_availability, 3.0) picking.move_line_ids.qty_done = 1.0 context = dict( @@ -96,3 +100,15 @@ class TestPurolator(TransactionCase): 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) + + # Basic case: no qty done or packages or anything at all really + # it makes sense to be able to do 'something' in this case even if that + # is just an error + picking.send_to_shipper() + self.assertTrue(picking.carrier_tracking_ref) From fc9b01ba94384d9e877df496c2e4780f748e7c66 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 19 Sep 2022 21:39:00 +0000 Subject: [PATCH 10/25] [IMP] delivery_purolator: implement picking shipping, with tests --- .../models/delivery_purolator.py | 123 +++++++++++++-- .../models/purolator_services.py | 142 +++++++++++++++++- delivery_purolator/tests/test_purolator.py | 26 +++- 3 files changed, 272 insertions(+), 19 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index de016596..5c09cbb0 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -1,8 +1,11 @@ from odoo import fields, models, _ +from odoo.exceptions import UserError from .purolator_services import PurolatorClient import logging +_logger = logging.getLogger(__name__) + PUROLATOR_SERVICES = [ ('PurolatorExpress9AM', 'Purolator Express 9AM'), ('PurolatorExpress10:30AM', 'Purolator Express 10:30AM'), @@ -211,26 +214,124 @@ class ProviderPurolator(models.Model): ('purolator_account_number', '=', self.purolator_account_number), ], limit=1) return carrier + + 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): + addr.Name = partner.name if not partner.is_company else '' + 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._get_purolator_service() - # had_customs = False + service = self._purolator_service() for picking in pickings: picking_packages = self.get_to_ship_picking_packages(picking) if picking_packages is None: continue - # do the shipment! - package_labels = [] - for x in []: - res = res + [shipping_data] # bug! fill in with appropriate data - picking.carrier_tracking_ref = ','.join(package_labels) + 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) + + # TODO package level signature and insurance.... + # IF we cannot do this at the package level, then we must implement it here. + # We may need to warn that all packages will follow the same rule. + # //Define OptionsInformation + # //ResidentialSignatureDomestic + # $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 + # TODO switch to 'Receiver' or 'ThirdParty' if needed + + shipment_res = service.shipment_create(shipment) + _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, )) + + 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) + for n, blob in enumerate(self._purolator_extract_doc_blobs(doc_res), 1): + document_blobs.append(('PuroPackage-%s-%s.%s' % (pin, n, 'ZPL'), blob)) + else: + # retrieve shipment_pin document(s) + doc_res = service.document_by_pin(shipment_pin) + _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, 'ZPL'), 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': 0.0, # TODO How can we know?! + 'tracking_number': shipment_pin, + } + res.append(shipping_data) - # FIXME - shipping_data = {'exact_price': 1.0, - 'tracking_number': ''} - res.append(shipping_data) return res diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index bc69c095..740bfdd0 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -7,6 +7,36 @@ from odoo.exceptions import UserError class PurolatorClient(object): + + # clients and factories + _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') + 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') + 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 @@ -20,19 +50,20 @@ class PurolatorClient(object): session.auth = HTTPBasicAuth(self.api_key, self.password) self.transport = Transport(cache=SqliteCache(), session=session) - def _get_client(self, wsdl_path): + def _get_client(self, wsdl_path, version='2.0'): + # 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='2.0', + Version=version, Language='en', GroupID='xxx', - RequestReference='RatingExample', + RequestReference='RatingExample', # TODO need to paramatarize this or something, doesn't make sense to shipment, maybe GroupID UserToken=self.activation_key, ) client.set_default_soapheaders([header_value]) - return client + return client def get_quick_estimate(self, sender_postal_code, receiver_address, package_type, total_weight): """ Call GetQuickEstimate @@ -52,7 +83,7 @@ class PurolatorClient(object): SenderPostalCode=sender_postal_code, ReceiverAddress=receiver_address, PackageType=package_type, - TotalWeight={ + TotalWeight={ # TODO FIX/paramatarize 'Value': 10.0, 'WeightUnit': 'lb', }, @@ -73,3 +104,104 @@ class PurolatorClient(object): 'shipments': False, 'error': 'Purolator service did not return any matching rates.', } + + 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 shipment + + def shipment_add_picking_packages(self, 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 + if not packages: + # setup default package + package_weight = picking.shipping_weight # TODO need conversion (lb) below + if package_weight < 1.0: + package_weight = 1.0 + total_weight_value += package_weight + package_type = carrier.purolator_default_package_type_id + p = self.shipping_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) + else: + for package in packages: + package_weight = package.shipping_weight # TODO need conversion (lb) below + if package_weight < 1.0: + package_weight = 1.0 + package_type = package.package_type_id + total_weight_value += package_weight + p = self.shipping_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', + }, + ) + # TODO p.Options.OptionIDValuePair (ID='SpecialHandling', Value='true') + # can we do per-package signature requirements? + # Packaging specific codes? + 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 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/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index f9157cf7..c5bd9f2d 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -1,5 +1,6 @@ from odoo.tests.common import Form, TransactionCase +from odoo.exceptions import UserError class TestPurolator(TransactionCase): @@ -11,9 +12,18 @@ class TestPurolator(TransactionCase): 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': 'Canadian Address', + '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, @@ -23,6 +33,7 @@ class TestPurolator(TransactionCase): 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', @@ -106,9 +117,18 @@ class TestPurolator(TransactionCase): 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 even if that - # is just an error + # it makes sense to be able to do 'something' in this case picking.send_to_shipper() self.assertTrue(picking.carrier_tracking_ref) + self.assertEqual(picking.message_attachment_count, 1) # has tracking label now From 57cde35521498ca34b5ca27c3b678e91cc97f55e Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 19 Sep 2022 23:16:06 +0000 Subject: [PATCH 11/25] [ADD] delivery_partner_purolator: add for 15.0 --- delivery_partner_purolator/README.rst | 22 +++++++++++++++++++ delivery_partner_purolator/__init__.py | 1 + delivery_partner_purolator/__manifest__.py | 17 ++++++++++++++ delivery_partner_purolator/models/__init__.py | 1 + delivery_partner_purolator/models/delivery.py | 7 ++++++ delivery_partner_purolator/tests/__init__.py | 1 + .../tests/test_purolator_account.py | 20 +++++++++++++++++ 7 files changed, 69 insertions(+) create mode 100644 delivery_partner_purolator/README.rst create mode 100644 delivery_partner_purolator/__init__.py create mode 100755 delivery_partner_purolator/__manifest__.py create mode 100644 delivery_partner_purolator/models/__init__.py create mode 100644 delivery_partner_purolator/models/delivery.py create mode 100644 delivery_partner_purolator/tests/__init__.py create mode 100644 delivery_partner_purolator/tests/test_purolator_account.py diff --git a/delivery_partner_purolator/README.rst b/delivery_partner_purolator/README.rst new file mode 100644 index 00000000..7438616a --- /dev/null +++ b/delivery_partner_purolator/README.rst @@ -0,0 +1,22 @@ +******************************************* +Hibou - Purolator Partner Shipping Accounts +******************************************* + +Adds Purolator shipping accounts. + +For more information and add-ons, visit `Hibou.io `_. + + +============= +Main Features +============= + +* Adds Purolator to the delivery type selection field. + +======= +License +======= + +Please see `LICENSE `_. + +Copyright Hibou Corp. 2022 diff --git a/delivery_partner_purolator/__init__.py b/delivery_partner_purolator/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_partner_purolator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_partner_purolator/__manifest__.py b/delivery_partner_purolator/__manifest__.py new file mode 100755 index 00000000..9419f4ac --- /dev/null +++ b/delivery_partner_purolator/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': 'Purolator Partner Shipping Accounts', + 'author': 'Hibou Corp. ', + 'version': '15.0.1.0.0', + 'license': 'LGPL-3', + 'category': 'Stock', + 'sequence': 95, + 'summary': 'Purolator Partner Shipping Accounts', + 'website': 'https://hibou.io/', + 'depends': [ + 'delivery_partner', + ], + 'data': [ + ], + 'installable': True, + 'application': False, +} diff --git a/delivery_partner_purolator/models/__init__.py b/delivery_partner_purolator/models/__init__.py new file mode 100644 index 00000000..be8cabd6 --- /dev/null +++ b/delivery_partner_purolator/models/__init__.py @@ -0,0 +1 @@ +from . import delivery diff --git a/delivery_partner_purolator/models/delivery.py b/delivery_partner_purolator/models/delivery.py new file mode 100644 index 00000000..5f1a03ee --- /dev/null +++ b/delivery_partner_purolator/models/delivery.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class PartnerShippingAccount(models.Model): + _inherit = 'partner.shipping.account' + + delivery_type = fields.Selection(selection_add=[('purolator', 'Purolator')], ondelete={'purolator': 'set default'}) diff --git a/delivery_partner_purolator/tests/__init__.py b/delivery_partner_purolator/tests/__init__.py new file mode 100644 index 00000000..edd5b88a --- /dev/null +++ b/delivery_partner_purolator/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purolator_account diff --git a/delivery_partner_purolator/tests/test_purolator_account.py b/delivery_partner_purolator/tests/test_purolator_account.py new file mode 100644 index 00000000..c52c917e --- /dev/null +++ b/delivery_partner_purolator/tests/test_purolator_account.py @@ -0,0 +1,20 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + + +class TestAccount(TransactionCase): + + def setUp(self): + super(TestAccount, self).setUp() + self.PartnerShippingAccount = self.env['partner.shipping.account'] + self.partner = self.env.ref('base.res_partner_12') + + def test_purolator_account_information(self): + # Create object and confirm that validation error raises if fedex account is blank or not 8 digits + _ = self.PartnerShippingAccount.create({ + 'name': '123456789', + 'description': 'Test Account', + 'partner_id': self.partner.id, + 'delivery_type': 'purolator', + 'note': 'This is a note' + }) From 4475b9cd8826e842b8b54fb72dc003a350068c34 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 19 Sep 2022 23:18:27 +0000 Subject: [PATCH 12/25] [IMP] delivery_purolator: refactors, add 3rd party, fix estimated weight --- .../models/delivery_purolator.py | 70 +++++++++++-------- .../models/purolator_services.py | 41 +++++++---- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 5c09cbb0..74cd81eb 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -83,13 +83,22 @@ class ProviderPurolator(models.Model): 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_weight(self, weight): + 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): + 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) 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 = { @@ -99,15 +108,9 @@ class ProviderPurolator(models.Model): 'PostalCode': receiver.zip, } # TODO packaging volume/length/width/height - weight = self._purolator_weight(order._get_estimated_weight()) - client = PurolatorClient( - self.purolator_api_key, - self.purolator_password, - self.purolator_activation_key, - self.purolator_account_number, - self.prod_environment, - ) - res = client.get_quick_estimate( + 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, @@ -130,7 +133,7 @@ class ProviderPurolator(models.Model): } return { 'success': True, - 'price': shipment[0]['TotalPrice'], + 'price': shipment[0]['TotalPrice'] if not third_party else 0.0, 'error_message': False, 'warning_message': False, } @@ -145,6 +148,7 @@ class ProviderPurolator(models.Model): return rates def _purolator_rate_shipment_multi_package(self, order=None, picking=None, package=None): + 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 = { @@ -170,15 +174,9 @@ class ProviderPurolator(models.Model): 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_weight(weight) - client = PurolatorClient( - self.purolator_api_key, - self.purolator_password, - self.purolator_activation_key, - self.purolator_account_number, - self.prod_environment, - ) - res = client.get_quick_estimate(sender.zip, receiver_address, package_code, 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']: return [{'carrier': self, 'success': False, @@ -195,7 +193,7 @@ class ProviderPurolator(models.Model): 'carrier': carrier, 'package': package or self.env['stock.quant.package'].browse(), 'success': True, - 'price': price, + '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, @@ -215,6 +213,14 @@ class ProviderPurolator(models.Model): ], 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, @@ -294,10 +300,15 @@ class ProviderPurolator(models.Model): shipment.PaymentInformation.PaymentType = 'Sender' shipment.PaymentInformation.RegisteredAccountNumber = self.purolator_account_number shipment.PaymentInformation.BillingAccountNumber = self.purolator_account_number - # TODO switch to 'Receiver' or 'ThirdParty' if needed + 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 - shipment_res = service.shipment_create(shipment) - _logger.info('purolator service.shipment_create for shipment %s resulted in %s' % (shipment, shipment_res)) + 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: @@ -312,21 +323,20 @@ class ProviderPurolator(models.Model): for p, pin in zip(picking_packages, piece_pins): pin = pin.Value p.carrier_tracking_ref = pin - doc_res = service.document_by_pin(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, 'ZPL'), blob)) + document_blobs.append(('PuroPackage-%s-%s.%s' % (pin, n, self.purolator_label_file_type), blob)) else: # retrieve shipment_pin document(s) - doc_res = service.document_by_pin(shipment_pin) - _logger.info('purolator service.document_by_pin for pin %s resulted in %s' % (shipment_pin, doc_res)) + 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, 'ZPL'), blob)) + document_blobs.append(('PuroShipment-%s-%s.%s' % (shipment_pin, n, self.purolator_label_file_type), 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': 0.0, # TODO How can we know?! diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index 740bfdd0..0618a6cd 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -9,11 +9,27 @@ from odoo.exceptions import UserError 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') + self._shipping_client = self._get_client('/EWS/V2/Shipping/ShippingService.asmx?wsdl', + request_reference='Shipping') return self._shipping_client _shipping_factory = None @@ -27,7 +43,9 @@ class PurolatorClient(object): @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') + 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 @@ -50,7 +68,7 @@ class PurolatorClient(object): session.auth = HTTPBasicAuth(self.api_key, self.password) self.transport = Transport(cache=SqliteCache(), session=session) - def _get_client(self, wsdl_path, version='2.0'): + 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) @@ -58,8 +76,8 @@ class PurolatorClient(object): header_value = request_context( Version=version, Language='en', - GroupID='xxx', - RequestReference='RatingExample', # TODO need to paramatarize this or something, doesn't make sense to shipment, maybe GroupID + GroupID='xxx', # TODO should we have a GroupID? + RequestReference=request_reference, UserToken=self.activation_key, ) client.set_default_soapheaders([header_value]) @@ -77,16 +95,15 @@ class PurolatorClient(object): :param total_weight: float (in pounds) :returns: dict {'shipments': list, 'error': string or False} """ - client = self._get_client('/EWS/V2/Estimating/EstimatingService.asmx?wsdl') - response = client.service.GetQuickEstimate( + response = self.estimating_client.service.GetQuickEstimate( BillingAccountNumber=self.account_number, SenderPostalCode=sender_postal_code, ReceiverAddress=receiver_address, PackageType=package_type, - TotalWeight={ # TODO FIX/paramatarize - 'Value': 10.0, + TotalWeight={ + 'Value': total_weight, 'WeightUnit': 'lb', - }, + }, ) errors = response['body']['ResponseInformation']['Errors'] if errors: @@ -128,7 +145,7 @@ class PurolatorClient(object): total_pieces = len(packages) or 1 if not packages: # setup default package - package_weight = picking.shipping_weight # TODO need conversion (lb) below + package_weight = carrier.purolator_convert_weight(picking.shipping_weight) if package_weight < 1.0: package_weight = 1.0 total_weight_value += package_weight @@ -154,7 +171,7 @@ class PurolatorClient(object): shipment.PackageInformation.PiecesInformation.Piece.append(p) else: for package in packages: - package_weight = package.shipping_weight # TODO need conversion (lb) below + package_weight = carrier.purolator_convert_weight(package.shipping_weight) if package_weight < 1.0: package_weight = 1.0 package_type = package.package_type_id From 1dc5218ab8a7244a213014ec52bbe3c0f269d37f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 00:48:16 +0000 Subject: [PATCH 13/25] [FIX] delivery_hibou: default attr for package_type --- delivery_hibou/models/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index b3f928e2..cc19fe83 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -31,10 +31,10 @@ class DeliveryCarrier(models.Model): return self._get_package_type_for_order(order, 'package_volume', 'volume') attr = getattr(self, '%s_default_packaging_id' % (self.delivery_type, ), None) if attr: - return attr() + return attr attr = getattr(self, '%s_default_package_type_id' % (self.delivery_type, ), None) if attr: - return attr() + return attr return self.env['stock.package.type'] def _get_package_type_for_order(self, order, package_type_field, product_field): From cd02f70adc2942ea084f933c053eb8f7543c2e83 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 01:00:05 +0000 Subject: [PATCH 14/25] [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', From ea3d1dddabfcaf77fbe04e68dce1a9dbd4035b4d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 02:52:35 +0000 Subject: [PATCH 15/25] [IMP] delivery_hibou: logging around package types, views Fix, default attr return. --- delivery_hibou/models/delivery.py | 25 +++++++++++++++++++++---- delivery_hibou/views/stock_views.xml | 13 +++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index cc19fe83..220eaae3 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,9 +1,13 @@ +import logging from odoo import api, fields, models, _ from odoo.tools.float_utils import float_compare from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) + + class DeliveryCarrier(models.Model): _inherit = 'delivery.carrier' @@ -26,19 +30,31 @@ class DeliveryCarrier(models.Model): # Package selection def get_package_type_for_order(self, order): if self.package_by_field == 'weight': - return self._get_package_type_for_order(order, 'max_weight', 'weight') + res = self._get_package_type_for_order(order, 'max_weight', 'weight') + _logger.info(' get_package_type_for_order package by weight (%s) %s' % (res.id, res.name)) + return res elif self.package_by_field == 'volume': - return self._get_package_type_for_order(order, 'package_volume', 'volume') + res = self._get_package_type_for_order(order, 'package_volume', 'volume') + _logger.info(' get_package_type_for_order package by volume (%s) %s' % (res.id, res.name)) + return res attr = getattr(self, '%s_default_packaging_id' % (self.delivery_type, ), None) if attr: + _logger.info(' get_package_type_for_order package by default_packaging_id (%s) %s' % (attr.id, attr.name)) return attr attr = getattr(self, '%s_default_package_type_id' % (self.delivery_type, ), None) if attr: + _logger.info(' get_package_type_for_order package by default_package_type_id (%s) %s' % (attr.id, attr.name)) return attr + _logger.info(' package by NULL') return self.env['stock.package.type'] def _get_package_type_for_order(self, order, package_type_field, product_field): - order_total = sum(order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')).mapped(lambda ol: ol.product_id[product_field] * ol.product_uom_qty)) + # NOTE do not optimize this into non-loop. + # this may be an orderfake + order_total = 0.0 + for ol in order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')): + order_total += ol.product_id[product_field] * ol.product_uom_qty + _logger.info(' _get_package_type_for_order order_total ' + str(order_total)) if order_total: package_types = self.env['stock.package.type'].search([ ('package_carrier_type', 'in', ('none', False, self.delivery_type)), @@ -49,6 +65,7 @@ class DeliveryCarrier(models.Model): if package_type[package_type_field] >= order_total: return package_type return package_types if not package_type else package_type + return self.env['stock.package.type'] # Utility def get_to_ship_picking_packages(self, picking): @@ -268,7 +285,7 @@ class DeliveryCarrier(models.Model): else: if packages: raise UserError(_('Cannot rate package without picking.')) - self = self.with_context(date_planned=('date_planned' in order._fields and order.date_planned or fields.Datetime.now())) + self = self.with_context(date_planned=('date_planned' in self.env['sale.order']._fields and order.date_planned or fields.Datetime.now())) res = [] for carrier in self: diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml index 0989f519..4205779e 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -52,4 +52,17 @@
+ + hibou.stock.package.type.form + stock.package.type + + + + + + + + + + From e1ffd6ad094e128c22364d4992a2fae07f1c0f09 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 02:56:15 +0000 Subject: [PATCH 16/25] [IMP] delivery_purolator: multi handling, and rating Now tested with Sale Order Planner and Delivery Planner. --- .../models/delivery_purolator.py | 22 ++++++++++--------- .../views/delivery_purolator_views.xml | 2 ++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index d1999cb2..4b1aca91 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -103,10 +103,10 @@ class ProviderPurolator(models.Model): if res.get('carrier') == self: if downgrade_response: return { - 'success': True, + 'success': res.get('success', True), 'price': res.get('price', 0.0), - 'error_message': False, - 'warning_message': False, + 'error_message': res.get('error_message', False), + 'warning_message': res.get('warning_message', False), } return res return { @@ -129,9 +129,9 @@ class ProviderPurolator(models.Model): 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]) + 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') % (puro_errors, )) + 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): @@ -150,12 +150,9 @@ class ProviderPurolator(models.Model): sender = self.get_shipper_warehouse(order=order, picking=picking) receiver = self.get_recipient(order=order, picking=picking) - date_planned = fields.Date.today() + date_planned = fields.Datetime.now() 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() @@ -163,6 +160,8 @@ class ProviderPurolator(models.Model): 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) @@ -201,7 +200,7 @@ class ProviderPurolator(models.Model): 'error_message': False, 'warning_message': _('TotalCharge not found.') if price == 0.0 else False, 'date_planned': date_planned, - 'date_delivered': fields.Date.to_date(shipment['ExpectedDeliveryDate']), + 'date_delivered': fields.Datetime.to_datetime(shipment['ExpectedDeliveryDate']), 'transit_days': shipment['EstimatedTransitDays'], 'service_code': shipment['ServiceID'], }) @@ -339,3 +338,6 @@ class ProviderPurolator(models.Model): res.append(shipping_data) return res + + # TODO cancel shipment + # TODO track shipment diff --git a/delivery_purolator/views/delivery_purolator_views.xml b/delivery_purolator/views/delivery_purolator_views.xml index 695f56dd..413823e0 100644 --- a/delivery_purolator/views/delivery_purolator_views.xml +++ b/delivery_purolator/views/delivery_purolator_views.xml @@ -15,6 +15,8 @@ + + From c5fd3dddb916a6ea223aef304ca71c7a8bee5776 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 19:45:24 +0000 Subject: [PATCH 17/25] [IMP] delivery_hibou: improve dynamic calls --- delivery_hibou/models/delivery.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 220eaae3..20f98e44 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -294,16 +294,16 @@ class DeliveryCarrier(models.Model): p.package_type_id.package_carrier_type in (False, '', 'none', carrier.delivery_type)) if packages and not carrier_packages: continue - if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type): + attr = getattr(carrier, '%s_rate_shipment_multi' % self.delivery_type, None) + if attr: try: - res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, - picking=picking, - packages=carrier_packages) + res += attr(order=order, picking=picking, packages=carrier_packages) except TypeError: # TODO remove catch if after Odoo 14 # This is intended to find ones that don't support packages= kwarg - res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, - picking=picking) + res2 = attr(order=order, picking=picking) + if res2: + res += res2 return res @@ -314,11 +314,12 @@ class DeliveryCarrier(models.Model): :param packages: Optional recordset of packages (should be for this carrier) ''' self.ensure_one() - if hasattr(self, '%s_cancel_shipment' % self.delivery_type): + attr = getattr(self, '%s_cancel_shipment' % self.delivery_type, None) + if attr: # No good way to tell if this method takes the kwarg for packages if packages: try: - return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings, packages=packages) + return attr(pickings, packages=packages) except TypeError: # we won't be able to cancel the packages properly # here we will TRY to make a good call here where we put the package references into the picking @@ -329,7 +330,7 @@ class DeliveryCarrier(models.Model): 'carrier_tracking_ref': tracking_ref, }) - return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) + return attr(pickings) class ChooseDeliveryPackage(models.TransientModel): From aa9f0cba52113c1596cb0320aa3c0be05948cc9b Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 20 Sep 2022 19:48:31 +0000 Subject: [PATCH 18/25] [IMP] delivery_purolator: add tracking and cancel shipment --- .../models/delivery_purolator.py | 25 +++++++++++++++++-- .../models/purolator_services.py | 6 +++++ delivery_purolator/tests/test_purolator.py | 4 +++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 4b1aca91..4aab75b9 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -339,5 +339,26 @@ class ProviderPurolator(models.Model): return res - # TODO cancel shipment - # TODO track shipment + 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 index 417a0823..109ba771 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -275,6 +275,12 @@ class PurolatorClient(object): ) 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() diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index c5bd9f2d..47d39f44 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -132,3 +132,7 @@ class TestPurolator(TransactionCase): picking.send_to_shipper() self.assertTrue(picking.carrier_tracking_ref) self.assertEqual(picking.message_attachment_count, 1) # has tracking label now + + # Void + picking.cancel_shipment() + self.assertFalse(picking.carrier_tracking_ref) From 97e4ee718343c1963c1a27b38bd3099098e5c5fe Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 21 Sep 2022 00:25:51 +0000 Subject: [PATCH 19/25] [IMP] delivery_purolator: rework packaging, support packaging codes, tests --- .../data/delivery_purolator_data.xml | 18 ++++++-- .../data/delivery_purolator_demo.xml | 4 +- .../models/delivery_purolator.py | 8 ++-- .../models/purolator_services.py | 41 +++++++++++++++++-- delivery_purolator/tests/test_purolator.py | 4 ++ 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/delivery_purolator/data/delivery_purolator_data.xml b/delivery_purolator/data/delivery_purolator_data.xml index 503d26de..527dd1c0 100644 --- a/delivery_purolator/data/delivery_purolator_data.xml +++ b/delivery_purolator/data/delivery_purolator_data.xml @@ -2,9 +2,21 @@ - - Purolator Customer Packaging - CustomerPackaging + + 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 index 7388c15d..ede6dd63 100644 --- a/delivery_purolator/data/delivery_purolator_demo.xml +++ b/delivery_purolator/data/delivery_purolator_demo.xml @@ -20,7 +20,7 @@ purolator PurolatorExpress - + @@ -39,7 +39,7 @@ purolator PurolatorGround - + diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 4aab75b9..758b2ece 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -1,3 +1,4 @@ +from base64 import b64decode from odoo import fields, models, _ from odoo.exceptions import UserError from .purolator_services import PurolatorClient @@ -253,7 +254,8 @@ class ProviderPurolator(models.Model): def _purolator_fill_address(self, addr, partner): - addr.Name = partner.name if not partner.is_company else '' + # 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) @@ -318,13 +320,13 @@ class ProviderPurolator(models.Model): 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), blob)) + 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), blob)) + 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) diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index 109ba771..a36f4b97 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -7,6 +7,15 @@ 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 @@ -151,9 +160,23 @@ class PurolatorClient(object): 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) + 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 weight = carrier.purolator_convert_weight(order._get_estimated_weight()) package_type_max_weight = 0.0 @@ -190,6 +213,9 @@ class PurolatorClient(object): '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(weight) shipment.PackageInformation.TotalWeight.WeightUnit = 'lb' @@ -215,6 +241,7 @@ class PurolatorClient(object): 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), @@ -233,6 +260,9 @@ class PurolatorClient(object): '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: @@ -240,6 +270,11 @@ class PurolatorClient(object): 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={ @@ -259,9 +294,9 @@ class PurolatorClient(object): 'DimensionUnit': 'in', }, ) - # TODO p.Options.OptionIDValuePair (ID='SpecialHandling', Value='true') - # can we do per-package signature requirements? - # Packaging specific codes? + 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) diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index 47d39f44..c316dd26 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -53,6 +53,10 @@ class TestPurolator(TransactionCase): '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') def _so_pick_shipping(self): # Regular Update Shipping functionality From 038c40854331974af4fffd0380d49a4088bcdc3f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 21 Sep 2022 19:22:51 +0000 Subject: [PATCH 20/25] [IMP] delivery_hibou: package count (special for package by volume) --- delivery_hibou/models/delivery.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 20f98e44..6e92d382 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,4 +1,5 @@ import logging +from math import ceil from odoo import api, fields, models, _ from odoo.tools.float_utils import float_compare from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES @@ -47,6 +48,14 @@ class DeliveryCarrier(models.Model): return attr _logger.info(' package by NULL') return self.env['stock.package.type'] + + def get_package_count_for_order(self, order, package_type=None): + if package_type is None: + package_type = self.get_package_type_for_order(order) + + if self.package_by_field == 'volume': + return self._get_package_count_for_order(order, package_type, 'package_volume', 'volume') + return self._get_package_count_for_order(order, package_type, 'max_weight', 'weight') def _get_package_type_for_order(self, order, package_type_field, product_field): # NOTE do not optimize this into non-loop. @@ -66,6 +75,17 @@ class DeliveryCarrier(models.Model): return package_type return package_types if not package_type else package_type return self.env['stock.package.type'] + + def _get_package_count_for_order(self, order, package_type, package_type_field, product_field): + # NOTE do not optimize this into non-loop. + # this may be an orderfake + order_total = 0.0 + for ol in order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')): + order_total += ol.product_id[product_field] * ol.product_uom_qty + package_type_field_value = package_type[package_type_field] + if not package_type_field_value or package_type_field_value >= order_total: + return 1.0 + return ceil(order_total / package_type_field_value) # Utility def get_to_ship_picking_packages(self, picking): @@ -204,7 +224,7 @@ class DeliveryCarrier(models.Model): def _get_shipper_warehouse_dropship_in(self, picking): if picking.sale_id: - picking.sale_id.partner_shipping_id + return picking.sale_id.partner_shipping_id return self._get_shipper_warehouse_dropship_in_no_sale(picking) def _get_shipper_warehouse_dropship_in_no_sale(self, picking): From c24af32792f5c6e4886e35ce234810f020fb33e3 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 21 Sep 2022 19:27:02 +0000 Subject: [PATCH 21/25] [IMP] delivery_purolator: use package count estimate --- .../models/purolator_services.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/delivery_purolator/models/purolator_services.py b/delivery_purolator/models/purolator_services.py index a36f4b97..9e276c73 100644 --- a/delivery_purolator/models/purolator_services.py +++ b/delivery_purolator/models/purolator_services.py @@ -176,24 +176,17 @@ class PurolatorClient(object): 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 - 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 - + 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 - total_weight_value = package_weight * total_pieces for _i in range(total_pieces): p = self.estimating_factory.Piece( Weight={ @@ -217,7 +210,7 @@ class PurolatorClient(object): self._add_piece_code(self.estimating_factory, p, package_code) shipment.PackageInformation.PiecesInformation.Piece.append(p) - shipment.PackageInformation.TotalWeight.Value = str(weight) + shipment.PackageInformation.TotalWeight.Value = str(total_weight_value) shipment.PackageInformation.TotalWeight.WeightUnit = 'lb' shipment.PackageInformation.TotalPieces = str(total_pieces) From 940f1e34088fc99297c8fd728a028a7fafb85071 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 21 Sep 2022 20:03:05 +0000 Subject: [PATCH 22/25] [FIX] delivery_hibou: cannot iterate on float --- delivery_hibou/models/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 6e92d382..3aeee13f 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -84,7 +84,7 @@ class DeliveryCarrier(models.Model): order_total += ol.product_id[product_field] * ol.product_uom_qty package_type_field_value = package_type[package_type_field] if not package_type_field_value or package_type_field_value >= order_total: - return 1.0 + return 1 return ceil(order_total / package_type_field_value) # Utility From 36b28cde42d73fafaab99d460ba2ef3a60160f57 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 21 Sep 2022 20:04:32 +0000 Subject: [PATCH 23/25] [IMP] delivery_purolator: declared value and signature required --- .../models/delivery_purolator.py | 101 +++++++++++------- delivery_purolator/tests/test_purolator.py | 3 + 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 758b2ece..11cc6fa0 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -7,6 +7,7 @@ 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'), @@ -36,39 +37,41 @@ PUROLATOR_SERVICES = [ ('PurolatorQuickShipEnvelope', 'Purolator Quick Ship Envelope'), ('PurolatorQuickShipPack', 'Purolator Quick Ship Pack'), ('PurolatorQuickShipBox', 'Purolator Quick Ship Box'), - ('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.'), - ('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'), + # 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'), ] @@ -145,6 +148,28 @@ class ProviderPurolator(models.Model): 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) @@ -175,6 +200,7 @@ class ProviderPurolator(models.Model): 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) @@ -294,15 +320,8 @@ class ProviderPurolator(models.Model): service.shipment_add_picking_packages(shipment, self, picking, picking_packages) - # TODO package level signature and insurance.... - # IF we cannot do this at the package level, then we must implement it here. - # We may need to warn that all packages will follow the same rule. - # //Define OptionsInformation - # //ResidentialSignatureDomestic - # $request->Shipment->PackageInformation->OptionsInformation->Options->OptionIDValuePair->ID = "ResidentialSignatureDomestic"; - # $request->Shipment->PackageInformation->OptionsInformation->Options->OptionIDValuePair->Value = "true"; - 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')) diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index c316dd26..efa37c0c 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -57,6 +57,9 @@ class TestPurolator(TransactionCase): # 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 From 06070eff9c782dfe1e8c2d5041a9c46e81d1088f Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Tue, 11 Oct 2022 19:19:27 -0500 Subject: [PATCH 24/25] [ADD] delivery_purolator_planner_price: get shipping costs from delivery planner H11065 --- delivery_purolator_planner_price/__init__.py | 3 + .../__manifest__.py | 28 +++++ .../tests/__init__.py | 3 + .../test_delivery_purolator_planner_price.py | 109 ++++++++++++++++++ .../wizard/__init__.py | 3 + .../wizard/stock_delivery_planner.py | 18 +++ 6 files changed, 164 insertions(+) create mode 100644 delivery_purolator_planner_price/__init__.py create mode 100644 delivery_purolator_planner_price/__manifest__.py create mode 100644 delivery_purolator_planner_price/tests/__init__.py create mode 100644 delivery_purolator_planner_price/tests/test_delivery_purolator_planner_price.py create mode 100644 delivery_purolator_planner_price/wizard/__init__.py create mode 100644 delivery_purolator_planner_price/wizard/stock_delivery_planner.py diff --git a/delivery_purolator_planner_price/__init__.py b/delivery_purolator_planner_price/__init__.py new file mode 100644 index 00000000..455a4c33 --- /dev/null +++ b/delivery_purolator_planner_price/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import wizard diff --git a/delivery_purolator_planner_price/__manifest__.py b/delivery_purolator_planner_price/__manifest__.py new file mode 100644 index 00000000..28cd1a2b --- /dev/null +++ b/delivery_purolator_planner_price/__manifest__.py @@ -0,0 +1,28 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Purolator Planner Price', + 'summary': 'Use estimated delivery cost when label is created.', + 'version': '15.0.1.0.1', + 'author': "Hibou Corp.", + 'category': 'Warehouse', + 'license': 'OPL-1', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +Purolator Planner Price +======================= + +* Uses estimated delivery cost when label is created. +""", + 'depends': [ + 'delivery_purolator', + 'stock_delivery_planner', + ], + 'demo': [ + ], + 'data': [ + ], + 'auto_install': True, + 'installable': True, +} diff --git a/delivery_purolator_planner_price/tests/__init__.py b/delivery_purolator_planner_price/tests/__init__.py new file mode 100644 index 00000000..04c314a3 --- /dev/null +++ b/delivery_purolator_planner_price/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import test_delivery_purolator_planner_price diff --git a/delivery_purolator_planner_price/tests/test_delivery_purolator_planner_price.py b/delivery_purolator_planner_price/tests/test_delivery_purolator_planner_price.py new file mode 100644 index 00000000..ea098d72 --- /dev/null +++ b/delivery_purolator_planner_price/tests/test_delivery_purolator_planner_price.py @@ -0,0 +1,109 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo.tests.common import Form, TransactionCase + + +class TestDeliveryPurolatorPlannerPrice(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.') + + # Order planner setup + self.env['ir.config_parameter'].sudo().set_param('sale.planner.carrier_ids.%s' % (self.env.company.id, ), + "%d" % self.carrier.id) + self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_ids.%s' % (self.env.company.id, ), + "%d" % self.carrier.id) + delivery_calendar = self.env['resource.calendar'].create({ + 'name': 'Test Delivery Calendar', + 'tz': 'US/Central', + 'attendance_ids': [ + (0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + ], + }) + self.carrier.delivery_calendar_id = delivery_calendar + # self.fedex_2_day.delivery_calendar_id = delivery_calendar + # self.env['stock.warehouse'].search([]).write({'shipping_calendar_id': delivery_calendar.id}) + + # 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', + 'shipping_calendar_id': delivery_calendar.id, + }) + self.env['ir.config_parameter'].sudo().set_param('sale.planner.warehouse_ids.%s' % (self.env.company.id, ), + "%d" % self.shipper_warehouse.id) + 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.0 # 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, + })], + }) + order_plan_action = self.sale_order.action_planorder() + order_plan = self.env[order_plan_action['res_model']].browse(order_plan_action['res_id']) + order_plan.planning_option_ids.filtered(lambda o: o.carrier_id == self.carrier).select_plan() + + self.sale_order.action_confirm() + self.picking = self.sale_order.picking_ids + + def test_00_estimate_shipping_cost(self): + self.assertEqual(self.picking.carrier_id, self.carrier, 'Carrier did not carry over to Delivery Order') + + self.picking.move_line_ids.filtered(lambda ml: ml.product_id == self.storage_box).qty_done = 3.0 + packing_action = self.picking.action_put_in_pack() + packing_wizard = Form(self.env[packing_action['res_model']].with_context(packing_action['context'])) + choose_delivery_package = packing_wizard.save() + choose_delivery_package.action_put_in_pack() + self.assertEqual(self.picking.shipping_weight, 3.0) + + action = self.picking.action_plan_delivery() + planner = self.env[action['res_model']].browse(action['res_id']) + + self.assertEqual(planner.picking_id, self.picking) + self.assertGreater(len(planner.plan_option_ids), 1) + + plan_option = planner.plan_option_ids.filtered(lambda o: o.carrier_id == self.carrier) + self.assertEqual(len(plan_option), 1) + self.assertGreater(plan_option.price, 0.0) + + plan_option.select_plan() + planner.action_plan() + self.assertEqual(self.picking.carrier_id, self.carrier) + self.assertEqual(plan_option.price, self.picking.carrier_price) diff --git a/delivery_purolator_planner_price/wizard/__init__.py b/delivery_purolator_planner_price/wizard/__init__.py new file mode 100644 index 00000000..09cfe093 --- /dev/null +++ b/delivery_purolator_planner_price/wizard/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import stock_delivery_planner diff --git a/delivery_purolator_planner_price/wizard/stock_delivery_planner.py b/delivery_purolator_planner_price/wizard/stock_delivery_planner.py new file mode 100644 index 00000000..43af07f7 --- /dev/null +++ b/delivery_purolator_planner_price/wizard/stock_delivery_planner.py @@ -0,0 +1,18 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import models + + +class StockDeliveryPlanner(models.TransientModel): + _inherit = 'stock.delivery.planner' + + def action_plan(self): + res = super().action_plan() + puro_package_options = self.plan_option_ids.filtered( + lambda o: (o.package_id + and o.selection == 'selected' + and o.carrier_id.delivery_type == 'purolator' + )) + if puro_package_options: + self.picking_id.carrier_price = sum(puro_package_options.mapped('price')) + return res From de92a0c9b8394e6f73753fe6da3b032afd17ecc3 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Tue, 11 Oct 2022 19:27:18 -0500 Subject: [PATCH 25/25] [IMP] delivery_purolator: leave `carrier_price` unchanged during shipping H11065 --- delivery_purolator/models/delivery_purolator.py | 2 +- delivery_purolator/tests/test_purolator.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/delivery_purolator/models/delivery_purolator.py b/delivery_purolator/models/delivery_purolator.py index 11cc6fa0..fe9de227 100644 --- a/delivery_purolator/models/delivery_purolator.py +++ b/delivery_purolator/models/delivery_purolator.py @@ -353,7 +353,7 @@ class ProviderPurolator(models.Model): picking.carrier_tracking_ref = shipment_pin shipping_data = { - 'exact_price': 0.0, # TODO How can we know?! + 'exact_price': picking.carrier_price, # price is set during planning 'tracking_number': shipment_pin, } res.append(shipping_data) diff --git a/delivery_purolator/tests/test_purolator.py b/delivery_purolator/tests/test_purolator.py index efa37c0c..f70295fa 100644 --- a/delivery_purolator/tests/test_purolator.py +++ b/delivery_purolator/tests/test_purolator.py @@ -136,9 +136,11 @@ class TestPurolator(TransactionCase): # 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()