From 2d4c21fddecff50e4b47376ed946e799e8ad77b9 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Mon, 19 Apr 2021 18:30:50 -0500 Subject: [PATCH 1/4] [ADD] delivery_easypost_hibou H1990 --- delivery_easypost_hibou/__init__.py | 1 + delivery_easypost_hibou/__manifest__.py | 18 ++++++ delivery_easypost_hibou/models/__init__.py | 1 + .../models/delivery_carrier.py | 63 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 delivery_easypost_hibou/__init__.py create mode 100644 delivery_easypost_hibou/__manifest__.py create mode 100644 delivery_easypost_hibou/models/__init__.py create mode 100644 delivery_easypost_hibou/models/delivery_carrier.py diff --git a/delivery_easypost_hibou/__init__.py b/delivery_easypost_hibou/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_easypost_hibou/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_easypost_hibou/__manifest__.py b/delivery_easypost_hibou/__manifest__.py new file mode 100644 index 00000000..a27a069e --- /dev/null +++ b/delivery_easypost_hibou/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Hibou EasyPost Shipping', + 'version': '13.0.1.0.0', + 'category': 'Stock', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + 'website': 'https://hibou.io/', + 'depends': [ + 'delivery_easypost', + 'delivery_hibou', + ], + 'data': [ + ], + 'demo': [ + ], + 'installable': True, + 'application': False, + } diff --git a/delivery_easypost_hibou/models/__init__.py b/delivery_easypost_hibou/models/__init__.py new file mode 100644 index 00000000..0a1fe29f --- /dev/null +++ b/delivery_easypost_hibou/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_carrier diff --git a/delivery_easypost_hibou/models/delivery_carrier.py b/delivery_easypost_hibou/models/delivery_carrier.py new file mode 100644 index 00000000..a42f1744 --- /dev/null +++ b/delivery_easypost_hibou/models/delivery_carrier.py @@ -0,0 +1,63 @@ +import requests +from odoo import models, _ +from odoo.exceptions import UserError +from odoo.addons.delivery_easypost.models.easypost_request import EasypostRequest +import logging +_logger = logging.getLogger(__name__) + + +class DeliveryCarrier(models.Model): + _inherit = 'delivery.carrier' + + def easypost_send_shipping(self, pickings): + """ It creates an easypost order and buy it with the selected rate on + delivery method or cheapest rate if it is not set. It will use the + packages used with the put in pack functionality or a single package if + the user didn't use packages. + Once the order is purchased. It will post as message the tracking + links and the shipping labels. + """ + res = [] + superself = self.sudo() + ep = EasypostRequest(self.sudo().easypost_production_api_key if self.prod_environment else self.sudo().easypost_test_api_key, self.log_xml) + for picking in pickings: + # Call Hibou delivery method to get picking type + is_return = superself._classify_picking(picking) in ('in', 'dropship_in',) + result = ep.send_shipping(self, picking.partner_id, picking.picking_type_id.warehouse_id.partner_id, + picking=picking, is_return=is_return) + if result.get('error_message'): + raise UserError(_(result['error_message'])) + rate = result.get('rate') + if rate['currency'] == picking.company_id.currency_id.name: + price = float(rate['rate']) + else: + quote_currency = self.env['res.currency'].search([('name', '=', rate['currency'])], limit=1) + price = quote_currency._convert(float(rate['rate']), picking.company_id.currency_id, self.env.company, fields.Date.today()) + + # return tracking information + carrier_tracking_link = "" + for track_number, tracker_url in result.get('track_shipments_url').items(): + carrier_tracking_link += '' + track_number + '
' + + carrier_tracking_ref = ' + '.join(result.get('track_shipments_url').keys()) + + labels = [] + for track_number, label_url in result.get('track_label_data').items(): + label = requests.get(label_url) + labels.append(('LabelEasypost-%s.%s' % (track_number, self.easypost_label_file_type), label.content)) + + logmessage = _("Shipment created into Easypost
" + "Tracking Numbers: %s
") % (carrier_tracking_link) + pickings.message_post(body=logmessage, attachments=labels) + + shipping_data = { + 'exact_price': price, + 'tracking_number': carrier_tracking_ref, + } + res = res + [shipping_data] + # store order reference on picking + picking.ep_order_ref = result.get('id') + if picking.carrier_id.return_label_on_delivery: + self.get_return_label(picking) + return res + From 1315ae0918caf25ea5393f120e3cb9e252a17c7a Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Wed, 5 May 2021 14:17:40 -0500 Subject: [PATCH 2/4] [IMP] delivery_easypost_hibou: allow easypost carriers to swap addresses H1990 --- delivery_easypost_hibou/__manifest__.py | 1 + .../models/delivery_carrier.py | 20 +++++++++++++------ .../views/delivery_carrier_views.xml | 13 ++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 delivery_easypost_hibou/views/delivery_carrier_views.xml diff --git a/delivery_easypost_hibou/__manifest__.py b/delivery_easypost_hibou/__manifest__.py index a27a069e..74bb30cd 100644 --- a/delivery_easypost_hibou/__manifest__.py +++ b/delivery_easypost_hibou/__manifest__.py @@ -10,6 +10,7 @@ 'delivery_hibou', ], 'data': [ + 'views/delivery_carrier_views.xml', ], 'demo': [ ], diff --git a/delivery_easypost_hibou/models/delivery_carrier.py b/delivery_easypost_hibou/models/delivery_carrier.py index a42f1744..157f842f 100644 --- a/delivery_easypost_hibou/models/delivery_carrier.py +++ b/delivery_easypost_hibou/models/delivery_carrier.py @@ -1,14 +1,17 @@ import requests -from odoo import models, _ +from odoo import fields, models, _ from odoo.exceptions import UserError from odoo.addons.delivery_easypost.models.easypost_request import EasypostRequest -import logging -_logger = logging.getLogger(__name__) class DeliveryCarrier(models.Model): _inherit = 'delivery.carrier' + easypost_return_method = fields.Selection([ + ('ep', 'EasyPost Return'), + ('swap', 'Swap Addresses') + ], string='Return Method', default='ep') + def easypost_send_shipping(self, pickings): """ It creates an easypost order and buy it with the selected rate on delivery method or cheapest rate if it is not set. It will use the @@ -22,9 +25,14 @@ class DeliveryCarrier(models.Model): ep = EasypostRequest(self.sudo().easypost_production_api_key if self.prod_environment else self.sudo().easypost_test_api_key, self.log_xml) for picking in pickings: # Call Hibou delivery method to get picking type - is_return = superself._classify_picking(picking) in ('in', 'dropship_in',) - result = ep.send_shipping(self, picking.partner_id, picking.picking_type_id.warehouse_id.partner_id, - picking=picking, is_return=is_return) + if superself.easypost_return_method == 'ep': + is_return = superself._classify_picking(picking) in ('in', 'dropship_in',) + result = ep.send_shipping(self, picking.partner_id, picking.picking_type_id.warehouse_id.partner_id, + picking=picking, is_return=is_return) + else: + shipper = superself.get_shipper_warehouse(picking=picking) + recipient = superself.get_recipient(picking=picking) + result = ep.send_shipping(self, recipient, shipper, picking=picking) if result.get('error_message'): raise UserError(_(result['error_message'])) rate = result.get('rate') diff --git a/delivery_easypost_hibou/views/delivery_carrier_views.xml b/delivery_easypost_hibou/views/delivery_carrier_views.xml new file mode 100644 index 00000000..62bba7cf --- /dev/null +++ b/delivery_easypost_hibou/views/delivery_carrier_views.xml @@ -0,0 +1,13 @@ + + + + delivery.carrier.form.inherit.delivery.easypost.hibou + delivery.carrier + + + + + + + + From 464d0c85de7e680770fc93d41c9ccece6759903f Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Fri, 14 May 2021 11:23:33 -0500 Subject: [PATCH 3/4] [IMP] delivery_easypost_hibou: add patch to prevent sending customs for same-country shipments H1990 --- delivery_easypost_hibou/models/__init__.py | 1 + .../models/easypost_patch.py | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 delivery_easypost_hibou/models/easypost_patch.py diff --git a/delivery_easypost_hibou/models/__init__.py b/delivery_easypost_hibou/models/__init__.py index 0a1fe29f..9a8a25e8 100644 --- a/delivery_easypost_hibou/models/__init__.py +++ b/delivery_easypost_hibou/models/__init__.py @@ -1 +1,2 @@ from . import delivery_carrier +from . import easypost_patch diff --git a/delivery_easypost_hibou/models/easypost_patch.py b/delivery_easypost_hibou/models/easypost_patch.py new file mode 100644 index 00000000..26a00584 --- /dev/null +++ b/delivery_easypost_hibou/models/easypost_patch.py @@ -0,0 +1,133 @@ +from odoo.tools.float_utils import float_round, float_is_zero +from odoo.addons.delivery_easypost.models.easypost_request import EasypostRequest + +# Patches to add customs lines during SO rating. + +def _prepare_order_shipments(self, carrier, order): + """ Method used in order to estimate delivery + cost for a quotation. It estimates the price with + the default package defined on the carrier. + e.g: if the default package on carrier is a 10kg Fedex + box and the customer ships 35kg it will create a shipment + with 4 packages (3 with 10kg and the last with 5 kg.). + It ignores reality with dimension or the fact that items + can not be cut in multiple pieces in order to allocate them + in different packages. It also ignores customs info. + """ + # Max weight for carrier default package + max_weight = carrier._easypost_convert_weight(carrier.easypost_default_packaging_id.max_weight) + # Order weight + total_weight = carrier._easypost_convert_weight( + sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line if not line.display_type])) + + # Create shipments + shipments = {} + if max_weight and total_weight > max_weight: + # Integer division for packages with maximal weight. + total_shipment = int(total_weight // max_weight) + # Remainder for last package. + last_shipment_weight = float_round(total_weight % max_weight, precision_digits=1) + for shp_id in range(0, total_shipment): + shipments.update(self._prepare_parcel(shp_id, carrier.easypost_default_packaging_id, max_weight, + carrier.easypost_label_file_type)) + shipments.update(self._customs_info_sale_order(shp_id, order.order_line)) + shipments.update(self._options(shp_id, carrier)) + if not float_is_zero(last_shipment_weight, precision_digits=1): + shipments.update( + self._prepare_parcel(total_shipment, carrier.easypost_default_packaging_id, last_shipment_weight, + carrier.easypost_label_file_type)) + shipments.update(self._customs_info_sale_order(shp_id, order.order_line)) + shipments.update(self._options(total_shipment, carrier)) + else: + shipments.update(self._prepare_parcel(0, carrier.easypost_default_packaging_id, total_weight, + carrier.easypost_label_file_type)) + shipments.update(self._customs_info_sale_order(0, order.order_line)) + shipments.update(self._options(0, carrier)) + return shipments + +def _customs_info_sale_order(self, shipment_id, lines): + """ generate a dict with customs info for each package... or each line + https://www.easypost.com/customs-guide.html + Currently general customs info for all packages are not generate. + For each shipment add a customs items by move line containing + - Product description (care it crash if bracket are used) + - Quantity for this product in the current package + - Product price + - Product price currency + - Total weight in ounces. + - Original country code(warehouse) + """ + customs_info = {} + customs_item_id = 0 + for line in lines: + # skip service + if line.product_id.type not in ['product', 'consu']: + continue + unit_quantity = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id, + rounding_method='HALF-UP') + hs_code = line.product_id.hs_code or '' + price_unit = line.price_reduce_taxinc + customs_info.update({ + 'order[shipments][%d][customs_info][customs_items][%d][description]' % ( + shipment_id, customs_item_id): line.product_id.name, + 'order[shipments][%d][customs_info][customs_items][%d][quantity]' % ( + shipment_id, customs_item_id): unit_quantity, + 'order[shipments][%d][customs_info][customs_items][%d][value]' % ( + shipment_id, customs_item_id): unit_quantity * price_unit, + 'order[shipments][%d][customs_info][customs_items][%d][currency]' % ( + shipment_id, customs_item_id): line.order_id.company_id.currency_id.name, + 'order[shipments][%d][customs_info][customs_items][%d][weight]' % (shipment_id, customs_item_id): + line.env['delivery.carrier']._easypost_convert_weight(line.product_id.weight * unit_quantity), + 'order[shipments][%d][customs_info][customs_items][%d][origin_country]' % ( + shipment_id, customs_item_id): line.order_id.warehouse_id.partner_id.country_id.code, + 'order[shipments][%d][customs_info][customs_items][%d][hs_tariff_number]' % ( + shipment_id, customs_item_id): hs_code, + }) + customs_item_id += 1 + return customs_info + +# Patch to prevent sending delivery customs for same-country-shipments +def _customs_info(self, shipment_id, lines): + """ generate a dict with customs info for each package. + https://www.easypost.com/customs-guide.html + Currently general customs info for all packages are not generate. + For each shipment add a customs items by move line containing + - Product description (care it crash if bracket are used) + - Quantity for this product in the current package + - Product price + - Product price currency + - Total weight in ounces. + - Original country code(warehouse) + """ + customs_info = {} + customs_item_id = 0 + for line in lines: + # Customization to return early if same country + # only need early return if one line does this + if line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code == line.picking_id.partner_id.country_id.code: + return {} + + # skip service + if line.product_id.type not in ['product', 'consu']: + continue + if line.picking_id.picking_type_code == 'incoming': + unit_quantity = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_id, rounding_method='HALF-UP') + else: + unit_quantity = line.product_uom_id._compute_quantity(line.qty_done, line.product_id.uom_id, rounding_method='HALF-UP') + hs_code = line.product_id.hs_code or '' + price_unit = line.move_id.sale_line_id.price_reduce_taxinc if line.move_id.sale_line_id else line.product_id.list_price + customs_info.update({ + 'order[shipments][%d][customs_info][customs_items][%d][description]' % (shipment_id, customs_item_id): line.product_id.name, + 'order[shipments][%d][customs_info][customs_items][%d][quantity]' % (shipment_id, customs_item_id): unit_quantity, + 'order[shipments][%d][customs_info][customs_items][%d][value]' % (shipment_id, customs_item_id): unit_quantity * price_unit, + 'order[shipments][%d][customs_info][customs_items][%d][currency]' % (shipment_id, customs_item_id): line.picking_id.company_id.currency_id.name, + 'order[shipments][%d][customs_info][customs_items][%d][weight]' % (shipment_id, customs_item_id): line.env['delivery.carrier']._easypost_convert_weight(line.product_id.weight * unit_quantity), + 'order[shipments][%d][customs_info][customs_items][%d][origin_country]' % (shipment_id, customs_item_id): line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code, + 'order[shipments][%d][customs_info][customs_items][%d][hs_tariff_number]' % (shipment_id, customs_item_id): hs_code, + }) + customs_item_id += 1 + return customs_info + +EasypostRequest._prepare_order_shipments = _prepare_order_shipments +EasypostRequest._customs_info_sale_order = _customs_info_sale_order +EasypostRequest._customs_info = _customs_info From 19e00765e24cd283374e6cff09652bfdec0acf40 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 13 May 2022 21:22:54 +0000 Subject: [PATCH 4/4] [MIG] delivery_easypost_hibou: to 15.0 --- delivery_easypost_hibou/__manifest__.py | 2 +- .../models/delivery_carrier.py | 21 +++++++++------- .../models/easypost_patch.py | 25 ++++++++----------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/delivery_easypost_hibou/__manifest__.py b/delivery_easypost_hibou/__manifest__.py index 74bb30cd..9105c619 100644 --- a/delivery_easypost_hibou/__manifest__.py +++ b/delivery_easypost_hibou/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Hibou EasyPost Shipping', - 'version': '13.0.1.0.0', + 'version': '15.0.1.0.0', 'category': 'Stock', 'author': "Hibou Corp.", 'license': 'AGPL-3', diff --git a/delivery_easypost_hibou/models/delivery_carrier.py b/delivery_easypost_hibou/models/delivery_carrier.py index 157f842f..66511d24 100644 --- a/delivery_easypost_hibou/models/delivery_carrier.py +++ b/delivery_easypost_hibou/models/delivery_carrier.py @@ -20,12 +20,13 @@ class DeliveryCarrier(models.Model): Once the order is purchased. It will post as message the tracking links and the shipping labels. """ - res = [] superself = self.sudo() + + res = [] ep = EasypostRequest(self.sudo().easypost_production_api_key if self.prod_environment else self.sudo().easypost_test_api_key, self.log_xml) for picking in pickings: # Call Hibou delivery method to get picking type - if superself.easypost_return_method == 'ep': + if self.easypost_return_method == 'ep': is_return = superself._classify_picking(picking) in ('in', 'dropship_in',) result = ep.send_shipping(self, picking.partner_id, picking.picking_type_id.warehouse_id.partner_id, picking=picking, is_return=is_return) @@ -33,8 +34,9 @@ class DeliveryCarrier(models.Model): shipper = superself.get_shipper_warehouse(picking=picking) recipient = superself.get_recipient(picking=picking) result = ep.send_shipping(self, recipient, shipper, picking=picking) + if result.get('error_message'): - raise UserError(_(result['error_message'])) + raise UserError(result['error_message']) rate = result.get('rate') if rate['currency'] == picking.company_id.currency_id.name: price = float(rate['rate']) @@ -56,16 +58,17 @@ class DeliveryCarrier(models.Model): logmessage = _("Shipment created into Easypost
" "Tracking Numbers: %s
") % (carrier_tracking_link) - pickings.message_post(body=logmessage, attachments=labels) + if picking.sale_id: + for pick in picking.sale_id.picking_ids: + pick.message_post(body=logmessage, attachments=labels) + else: + picking.message_post(body=logmessage, attachments=labels) - shipping_data = { - 'exact_price': price, - 'tracking_number': carrier_tracking_ref, - } + shipping_data = {'exact_price': price, + 'tracking_number': carrier_tracking_ref} res = res + [shipping_data] # store order reference on picking picking.ep_order_ref = result.get('id') if picking.carrier_id.return_label_on_delivery: self.get_return_label(picking) return res - diff --git a/delivery_easypost_hibou/models/easypost_patch.py b/delivery_easypost_hibou/models/easypost_patch.py index 26a00584..037aeff6 100644 --- a/delivery_easypost_hibou/models/easypost_patch.py +++ b/delivery_easypost_hibou/models/easypost_patch.py @@ -1,5 +1,6 @@ from odoo.tools.float_utils import float_round, float_is_zero from odoo.addons.delivery_easypost.models.easypost_request import EasypostRequest +from odoo.tools.float_utils import float_round, float_is_zero, float_repr # Patches to add customs lines during SO rating. @@ -15,10 +16,9 @@ def _prepare_order_shipments(self, carrier, order): in different packages. It also ignores customs info. """ # Max weight for carrier default package - max_weight = carrier._easypost_convert_weight(carrier.easypost_default_packaging_id.max_weight) + max_weight = carrier._easypost_convert_weight(carrier.easypost_default_package_type_id.max_weight) # Order weight - total_weight = carrier._easypost_convert_weight( - sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line if not line.display_type])) + total_weight = carrier._easypost_convert_weight(order._get_estimated_weight()) # Create shipments shipments = {} @@ -28,19 +28,15 @@ def _prepare_order_shipments(self, carrier, order): # Remainder for last package. last_shipment_weight = float_round(total_weight % max_weight, precision_digits=1) for shp_id in range(0, total_shipment): - shipments.update(self._prepare_parcel(shp_id, carrier.easypost_default_packaging_id, max_weight, - carrier.easypost_label_file_type)) + shipments.update(self._prepare_parcel(shp_id, carrier.easypost_default_package_type_id, max_weight, carrier.easypost_label_file_type)) shipments.update(self._customs_info_sale_order(shp_id, order.order_line)) shipments.update(self._options(shp_id, carrier)) if not float_is_zero(last_shipment_weight, precision_digits=1): - shipments.update( - self._prepare_parcel(total_shipment, carrier.easypost_default_packaging_id, last_shipment_weight, - carrier.easypost_label_file_type)) + shipments.update(self._prepare_parcel(total_shipment, carrier.easypost_default_package_type_id, last_shipment_weight, carrier.easypost_label_file_type)) shipments.update(self._customs_info_sale_order(shp_id, order.order_line)) shipments.update(self._options(total_shipment, carrier)) else: - shipments.update(self._prepare_parcel(0, carrier.easypost_default_packaging_id, total_weight, - carrier.easypost_label_file_type)) + shipments.update(self._prepare_parcel(0, carrier.easypost_default_package_type_id, total_weight, carrier.easypost_label_file_type)) shipments.update(self._customs_info_sale_order(0, order.order_line)) shipments.update(self._options(0, carrier)) return shipments @@ -106,7 +102,7 @@ def _customs_info(self, shipment_id, lines): # only need early return if one line does this if line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code == line.picking_id.partner_id.country_id.code: return {} - + # skip service if line.product_id.type not in ['product', 'consu']: continue @@ -114,12 +110,13 @@ def _customs_info(self, shipment_id, lines): unit_quantity = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_id, rounding_method='HALF-UP') else: unit_quantity = line.product_uom_id._compute_quantity(line.qty_done, line.product_id.uom_id, rounding_method='HALF-UP') + rounded_qty = max(1, float_round(unit_quantity, precision_digits=0, rounding_method='HALF-UP')) + rounded_qty = float_repr(rounded_qty, precision_digits=0) hs_code = line.product_id.hs_code or '' - price_unit = line.move_id.sale_line_id.price_reduce_taxinc if line.move_id.sale_line_id else line.product_id.list_price customs_info.update({ 'order[shipments][%d][customs_info][customs_items][%d][description]' % (shipment_id, customs_item_id): line.product_id.name, - 'order[shipments][%d][customs_info][customs_items][%d][quantity]' % (shipment_id, customs_item_id): unit_quantity, - 'order[shipments][%d][customs_info][customs_items][%d][value]' % (shipment_id, customs_item_id): unit_quantity * price_unit, + 'order[shipments][%d][customs_info][customs_items][%d][quantity]' % (shipment_id, customs_item_id): rounded_qty, + 'order[shipments][%d][customs_info][customs_items][%d][value]' % (shipment_id, customs_item_id): line.sale_price, 'order[shipments][%d][customs_info][customs_items][%d][currency]' % (shipment_id, customs_item_id): line.picking_id.company_id.currency_id.name, 'order[shipments][%d][customs_info][customs_items][%d][weight]' % (shipment_id, customs_item_id): line.env['delivery.carrier']._easypost_convert_weight(line.product_id.weight * unit_quantity), 'order[shipments][%d][customs_info][customs_items][%d][origin_country]' % (shipment_id, customs_item_id): line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code,