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..9105c619
--- /dev/null
+++ b/delivery_easypost_hibou/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'Hibou EasyPost Shipping',
+ 'version': '15.0.1.0.0',
+ 'category': 'Stock',
+ 'author': "Hibou Corp.",
+ 'license': 'AGPL-3',
+ 'website': 'https://hibou.io/',
+ 'depends': [
+ 'delivery_easypost',
+ 'delivery_hibou',
+ ],
+ 'data': [
+ 'views/delivery_carrier_views.xml',
+ ],
+ '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..9a8a25e8
--- /dev/null
+++ b/delivery_easypost_hibou/models/__init__.py
@@ -0,0 +1,2 @@
+from . import delivery_carrier
+from . import easypost_patch
diff --git a/delivery_easypost_hibou/models/delivery_carrier.py b/delivery_easypost_hibou/models/delivery_carrier.py
new file mode 100644
index 00000000..66511d24
--- /dev/null
+++ b/delivery_easypost_hibou/models/delivery_carrier.py
@@ -0,0 +1,74 @@
+import requests
+from odoo import fields, models, _
+from odoo.exceptions import UserError
+from odoo.addons.delivery_easypost.models.easypost_request import EasypostRequest
+
+
+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
+ 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.
+ """
+ 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 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)
+ 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')
+ 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)
+ 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}
+ 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
new file mode 100644
index 00000000..037aeff6
--- /dev/null
+++ b/delivery_easypost_hibou/models/easypost_patch.py
@@ -0,0 +1,130 @@
+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.
+
+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_package_type_id.max_weight)
+ # Order weight
+ total_weight = carrier._easypost_convert_weight(order._get_estimated_weight())
+
+ # 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_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_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_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
+
+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')
+ 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 ''
+ 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): 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,
+ '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
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
+
+
+
+
+
+
+
+