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 elif partner.parent_id and partner.parent_id.phone: addr['phone'] = partner.parent_id.phone # We had trouble communicating with the carrier: ShipFrom phone number cannot be more than 15 digits long if 'phone' in addr and len(addr['phone']) > 15: addr['phone'] = ''.join(c for c in addr['phone'] if c.isdigit()) 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' pkg['weight'] = self._get_weight_with_tare(order, picking) 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'] q = product_uom_obj._compute_qty_obj(self._get_default_uom(), line.product_uom_qty, self.uom_id) 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)) 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)) 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)) 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)) weight = ptempl.weight res = { 'origin_country': orig, 'description': line.name, 'hs_tariff_number': hts, 'quantity': q, 'value_amount': line.price_unit, 'net_weight': weight, } return res def build_sc_customs_decl(self, picking): items = [self.build_sc_customs_item(x) for x in picking.move_lines] 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 carrier_service = self.sudo().sc_carrier_service # 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 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) # 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'] @api.one 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 content_desc = pickings.sc_content_desc carrier_service = self.sudo().sc_carrier_service # 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(picking=pickings) customs = self.build_sc_customs_decl(pickings) # build the actual shipment object 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, descr=content_desc) api = self._shipcloud_api() try: #print("SHP: %s" % shp) result = api.create_shipment(shp, gen_label=True) #print("RES: %s" % result) except shipcloud.ApiError as err: raise Warning(err) # result = ["id", "carrier_tracking_no", "tracking_url", "label_url", "price"] # {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']}) # TODO: download label from label_url so it can be returned as attachment res = {'exact_price': result['price'], 'weight': pkg['weight'], 'date_delivery': None, 'tracking_number': ' ' + result['carrier_tracking_no'], 'attachments': [(filename, result['label_bin'])]} return res @api.one def sc_cancel_shipment(self, picking): """Cancel a shipping label""" api = self._shipcloud_api() api.delete_shipment(picking.sc_shipment_id) @api.one def sc_get_tracking_link(self, pickings): """Return a tracking link for the given picking""" return pickings.sc_tracking_url