2021-02-14 18:18:25 +00:00
|
|
|
from openerp import api, fields, models
|
|
|
|
import logging
|
|
|
|
from openerp.exceptions import Warning
|
|
|
|
import pycountry
|
|
|
|
|
|
|
|
import shipcloud
|
|
|
|
|
|
|
|
# FIXME: unify with odoo-internetmarke
|
|
|
|
# split the last word of a string containing stree name + house number
|
|
|
|
def split_street_house(streethouse):
|
|
|
|
# first try to split at last space
|
|
|
|
r = streethouse.rsplit(' ', 1)
|
|
|
|
# if that fails, try to split at last dot
|
|
|
|
if len(r) < 2:
|
|
|
|
r = streethouse.rsplit('.', 1)
|
|
|
|
# if that also fails, return empty house number
|
|
|
|
if len(r) < 2:
|
|
|
|
return (streethouse, '')
|
|
|
|
return (r[0], r[1])
|
|
|
|
|
|
|
|
def split_first_lastname(name):
|
|
|
|
# try to split at last space
|
|
|
|
r = name.rsplit(' ', 1)
|
|
|
|
# if this fails, simply claim everything is the last name
|
|
|
|
if len(r) < 2:
|
|
|
|
return ("", name)
|
|
|
|
return (r[0], r[1])
|
|
|
|
|
|
|
|
|
|
|
|
class SCDeliveryCarrier(models.Model):
|
|
|
|
_inherit = 'delivery.carrier'
|
|
|
|
|
|
|
|
def build_sc_addr(self, partner):
|
|
|
|
"""Convert an Odoo partner object into a shipcloud address."""
|
|
|
|
addr = {}
|
|
|
|
(street, house) = split_street_house(partner.street)
|
|
|
|
addr['street'] = street
|
|
|
|
addr['street_no'] = house
|
|
|
|
if partner.street2:
|
|
|
|
addr['care_of'] = partner.street2
|
|
|
|
addr['zip_code'] = partner.zip
|
|
|
|
addr['city'] = partner.city
|
|
|
|
if partner.state_id and partner.state_id.code:
|
|
|
|
addr['state'] = partner.state_id.code
|
|
|
|
addr['country'] = partner.country_id.code
|
|
|
|
|
|
|
|
if partner.is_company:
|
|
|
|
addr['company'] = partner.name
|
|
|
|
else:
|
|
|
|
if partner.parent_id.name:
|
|
|
|
addr['company'] = partner.parent_id.name
|
|
|
|
if partner.name:
|
|
|
|
(first, last) = split_first_lastname(partner.name)
|
|
|
|
addr['first_name'] = first
|
|
|
|
addr['last_name'] = last
|
|
|
|
if partner.email:
|
|
|
|
addr['email'] = partner.email
|
|
|
|
if partner.mobile:
|
|
|
|
addr['phone'] = partner.mobile
|
|
|
|
elif partner.phone:
|
|
|
|
addr['phone'] = partner.phone
|
|
|
|
return addr
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def estimate_dimensions(weight_kg, density_kg_per_dm3):
|
|
|
|
"""Estimate the dimensions of a given package, given its weight and mass density,
|
|
|
|
assuming a 3:2:1 ration between length:width:height"""
|
|
|
|
def cbrt(x):
|
|
|
|
"""Return cubic root of 'x'"""
|
|
|
|
return x**(1.0/3)
|
|
|
|
volume_dm3 = float(weight_kg) / float(density_kg_per_dm3)
|
|
|
|
volume_cm3 = 1000 * volume_dm3
|
|
|
|
# assuming l=3x, w=2x, h=1x -> x=6
|
|
|
|
x = cbrt(volume_cm3 / 6)
|
|
|
|
return (3.0*x, 2.0*x, x)
|
|
|
|
|
|
|
|
def build_sc_pkg(self, order=None, picking=None):
|
|
|
|
"""Convert an Odoo stock.picking or sale.order into a shipcloud package"""
|
|
|
|
pkg = {}
|
|
|
|
pkg['type'] = 'parcel'
|
2021-02-14 20:16:23 +00:00
|
|
|
pkg['weight'] = self._get_weight_with_tare(order, picking)
|
2021-02-14 18:18:25 +00:00
|
|
|
if picking:
|
|
|
|
pkg['length'] = picking.packaging_length
|
|
|
|
pkg['width'] = picking.packaging_width
|
|
|
|
pkg['height'] = picking.packaging_height
|
|
|
|
else:
|
|
|
|
# we assume an average mass density of 0.5kg per dm3 (litre)
|
|
|
|
est = self.estimate_dimensions(pkg['weight'], 0.5)
|
|
|
|
pkg['length'] = est[0]
|
|
|
|
pkg['width'] = est[1]
|
|
|
|
pkg['height'] = est[2]
|
|
|
|
return pkg
|
|
|
|
|
|
|
|
def build_sc_customs_item(self, line):
|
|
|
|
"""Generate a shipcloud customs_item from a stock.move (line of a picking)"""
|
|
|
|
product_uom_obj = self.env['product.uom']
|
2021-02-14 20:16:23 +00:00
|
|
|
q = product_uom_obj._compute_qty_obj(self._get_default_uom(), line.product_uom_qty, self.uom_id)
|
2021-02-14 18:18:25 +00:00
|
|
|
product = line.product_id
|
|
|
|
if product:
|
|
|
|
if product.x_sysmo_customs_code:
|
|
|
|
hts = product.x_sysmo_customs_code
|
|
|
|
else:
|
|
|
|
raise Warning('Product Variant %s has no HTS defined' % (product.name))
|
2021-02-14 20:16:23 +00:00
|
|
|
if product.x_country_of_origin:
|
|
|
|
orig = product.x_country_of_origin.code
|
|
|
|
elif line.product_tmpl_id and line.product_tmpl_id.x_country_of_origin:
|
|
|
|
orig = line.product_tmpl_id.x_country_of_origin.code
|
|
|
|
else:
|
|
|
|
raise Warning('Product Variant %s has no Country of Origin defined' % (product.name))
|
2021-02-14 18:18:25 +00:00
|
|
|
weight = product.weight
|
|
|
|
elif line.product_tmpl_id:
|
|
|
|
ptmpl = line.product_tmpl_id
|
|
|
|
if ptempl.x_sysmo_default_customs_code:
|
|
|
|
hts = ptempl.x_sysmo_default_customs_code
|
|
|
|
else:
|
|
|
|
raise Warning('Product %s has no HTS defined' % (ptempl.name))
|
2021-02-14 20:16:23 +00:00
|
|
|
if ptempl.x_country_of_origin:
|
|
|
|
orig = ptempl.x_country_of_origin.code
|
|
|
|
else:
|
|
|
|
raise Warning('Product %s has no Country of Origin defined' % (ptempl.name))
|
2021-02-14 18:18:25 +00:00
|
|
|
weight = ptempl.weight
|
|
|
|
res = {
|
|
|
|
'origin_country': orig,
|
|
|
|
'description': line.name,
|
|
|
|
'hs_tariff_number': hts,
|
|
|
|
'quantity': q,
|
|
|
|
'value_amount': line.price_unit,
|
|
|
|
'net_weight': weight,
|
|
|
|
}
|
2021-02-14 20:16:23 +00:00
|
|
|
return res
|
2021-02-14 18:18:25 +00:00
|
|
|
|
|
|
|
def build_sc_customs_decl(self, picking):
|
2021-02-14 20:16:23 +00:00
|
|
|
items = [self.build_sc_customs_item(x) for x in picking.move_lines]
|
2021-02-14 18:18:25 +00:00
|
|
|
total = 0.0
|
|
|
|
for i in items:
|
|
|
|
total += i['value_amount']
|
|
|
|
customs = {
|
|
|
|
'contents_type': 'commercial_goods',
|
|
|
|
'currency': currency,
|
|
|
|
'invoice_number': picking.name,
|
|
|
|
'total_value_amount': total,
|
|
|
|
'items': items
|
|
|
|
}
|
|
|
|
return customs
|
|
|
|
|
|
|
|
def _shipcloud_api(self):
|
|
|
|
config = self._get_config()
|
|
|
|
api_key = config['sc_api_key']
|
|
|
|
sandbox_api_key = None if config['sc_api_use_prod'] else config['sc_api_key_sandbox']
|
|
|
|
return shipcloud.api(api_key, sandbox_api_key)
|
|
|
|
|
|
|
|
# 'public' methods used by delivery_carrier
|
|
|
|
|
|
|
|
def sc_get_shipping_price_from_so(self, order):
|
|
|
|
"""Obtain a shipping quote for the given sale.order"""
|
|
|
|
recipient = order.partner_shipping_id if order.partner_shipping_id else order.partner_id
|
|
|
|
warehouse = order.warehouse_id.partner_id
|
2021-02-14 20:16:23 +00:00
|
|
|
carrier_service = self.sudo().sc_carrier_service
|
2021-02-14 18:18:25 +00:00
|
|
|
|
|
|
|
# build individual sub-objects of the shipment
|
|
|
|
from_addr = self.build_sc_addr(warehouse)
|
|
|
|
to_addr = self.build_sc_addr(recipient)
|
|
|
|
pkg = self.build_sc_pkg(order=order)
|
|
|
|
# build the actual shipment object
|
2021-02-14 20:16:23 +00:00
|
|
|
shp = shipcloud.gen_shipment(from_addr, to_addr, pkg, order.name,
|
|
|
|
carrier=carrier_service.carrier, service=carrier_service.service,
|
|
|
|
label_fmt=carrier_service.label_size)
|
2021-02-14 18:18:25 +00:00
|
|
|
# convert shipment to quote object
|
|
|
|
api = self._shipcloud_api()
|
|
|
|
try:
|
|
|
|
result = api.get_shipment_quote(shp)
|
|
|
|
except shipcloud.ApiError as err:
|
|
|
|
raise Warning(err)
|
|
|
|
# { "shipment_quote": { "price": 42.12 } }
|
|
|
|
return result['shipment_quote']['price']
|
|
|
|
|
2021-02-14 20:16:23 +00:00
|
|
|
@api.one
|
2021-02-14 18:18:25 +00:00
|
|
|
def sc_send_shipping(self, pickings):
|
|
|
|
"""Generate a shipping label from the given stock.picking"""
|
|
|
|
order = self.env['sale.order'].search([('name','=',pickings.origin)])
|
|
|
|
recipient = pickings.partner_id
|
|
|
|
warehouse = pickings.picking_type_id.warehouse_id.partner_id
|
2021-02-14 20:16:23 +00:00
|
|
|
carrier_service = self.sudo().sc_carrier_service
|
2021-02-14 18:18:25 +00:00
|
|
|
|
|
|
|
# build individual sub-objects of the shipment
|
|
|
|
from_addr = self.build_sc_addr(warehouse)
|
|
|
|
to_addr = self.build_sc_addr(recipient)
|
2021-02-14 20:16:23 +00:00
|
|
|
pkg = self.build_sc_pkg(picking=pickings)
|
2021-02-14 18:18:25 +00:00
|
|
|
customs = self.build_sc_customs_decl(pickings)
|
|
|
|
# build the actual shipment object
|
2021-02-14 20:16:23 +00:00
|
|
|
shp = shipcloud.gen_shipment(from_addr, to_addr, pkg, pickings.name, customs_decl=customs,
|
|
|
|
carrier=carrier_service.carrier, service=carrier_service.service,
|
|
|
|
label_fmt=carrier_service.label_size)
|
2021-02-14 18:18:25 +00:00
|
|
|
api = self._shipcloud_api()
|
|
|
|
try:
|
2021-02-14 20:16:23 +00:00
|
|
|
#print("SHP: %s" % shp)
|
|
|
|
result = api.create_shipment(shp, gen_label=True)
|
|
|
|
#print("RES: %s" % result)
|
2021-02-14 18:18:25 +00:00
|
|
|
except shipcloud.ApiError as err:
|
|
|
|
raise Warning(err)
|
|
|
|
|
|
|
|
# result = ["id", "carrier_tracking_no", "tracking_url", "label_url", "price"]
|
2021-02-14 20:16:23 +00:00
|
|
|
# {u'label_url': u'https://shipping-labels.shipcloud.io/shipments/a948e8c2/e3fb26be59/label/shipping_label_e3fb26be59.pdf', u'price': 0.0, u'id': u'e3fb26be59a68acd04d565dda027efd415ca8117', u'tracking_url': u'https://track.shipcloud.io/e3fb26be59a68acd04d565dda027efd415ca8117', u'carrier_tracking_no': u'1ZV306W00493609016'}
|
|
|
|
filename = '%s.pdf' % result['carrier_tracking_no']
|
|
|
|
pickings.update({'sc_shipment_id': result['id'],
|
|
|
|
'sc_tracking_url': result['tracking_url']})
|
2021-02-14 18:18:25 +00:00
|
|
|
# TODO: download label from label_url so it can be returned as attachment
|
|
|
|
|
|
|
|
res = {'exact_price': result['price'],
|
|
|
|
'weight': pkg['weight'],
|
2021-02-14 20:16:23 +00:00
|
|
|
'date_delivery': None,
|
|
|
|
'tracking_number': ' ' + result['carrier_tracking_no'],
|
|
|
|
'attachments': [(filename, result['label_bin'])]}
|
2021-02-14 18:18:25 +00:00
|
|
|
return res
|
|
|
|
|
|
|
|
|
2021-02-14 20:16:23 +00:00
|
|
|
@api.one
|
|
|
|
def sc_cancel_shipment(self, picking):
|
2021-02-14 18:18:25 +00:00
|
|
|
"""Cancel a shipping label"""
|
2021-02-14 20:16:23 +00:00
|
|
|
api = self._shipcloud_api()
|
|
|
|
api.delete_shipment(picking.sc_shipment_id)
|
2021-02-14 18:18:25 +00:00
|
|
|
|
2021-02-14 20:16:23 +00:00
|
|
|
@api.one
|
2021-02-14 18:18:25 +00:00
|
|
|
def sc_get_tracking_link(self, pickings):
|
|
|
|
"""Return a tracking link for the given picking"""
|
|
|
|
return pickings.sc_tracking_url
|