from openerp import api, fields, models import logging from openerp.exceptions import Warning import pycountry from inema import Internetmarke from inema import WarenpostInt _logger = logging.getLogger(__name__) TRACKING_URL = 'https://www.deutschepost.de/sendung/simpleQuery.html?locale=en_GB' # convert from ISO3166 2-digit to 3-digit def get_alpha3_country_from_alpha2(twodigit): c = pycountry.countries.get(alpha2=twodigit) return c.alpha3 # 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 DPDeliveryCarrier(models.Model): _inherit="delivery.carrier" def conn_auth_im(self): config = self._get_config() partner_id = config['dp_partner_id'] key = config['dp_key'] key_phase = config['dp_key_phase'] pk_user = config['dp_portokasse_user'] pk_passwd = config['dp_portokasse_passwd'] im = Internetmarke(partner_id, key, key_phase) im.authenticate(pk_user, pk_passwd) return im # Convert an Odoo Partner object into Internetmarke Address def build_im_addr(self, im, partner): (street, house) = split_street_house(partner.street) country = get_alpha3_country_from_alpha2(partner.country_id.code) street2 = None if partner.street2: street2 = partner.street2 # Countries like the US have state codes preceeding the ZIP if partner.state_id and partner.state_id.code: zipcode = "%s %s" % (partner.state_id.code, partner.zip) else: zipcode = partner.zip addr = im.build_addr(street = street, house = house, additional = street2, zipcode = zipcode, city = partner.city, country = country) if partner.is_company: return im.build_comp_addr(company = partner.name, address = addr) else: if partner.parent_id.name: person = None if partner.name: (first, last) = split_first_lastname(partner.name) title = None if partner.title and partner.title.shortcut: title = partner.title.shortcut person = im.build_pers_name(first=first, last=last, title=title) return im.build_comp_addr(company = partner.parent_id.name, address = addr, person = person) else: (first, last) = split_first_lastname(partner.name) return im.build_pers_addr(first = first, last = last, address = addr) def conn_auth_wpi(self): """Connect to the Warenpost International API""" config = self._get_config() partner_id = config['dp_partner_id'] key = config['dp_key'] key_phase = config['dp_key_phase'] pk_user = config['dp_portokasse_user'] pk_passwd = config['dp_portokasse_passwd'] ekp = config['dp_wpi_ekp'] use_sandbox = config['dp_wpi_sandbox'] print(__name__) print(partner_id, key, ekp, pk_user, pk_passwd, key_phase, use_sandbox) wpi = WarenpostInt(partner_id, key, ekp, pk_user, pk_passwd, key_phase, use_sandbox) wpi.get_token() return wpi def build_wpi_addr(self, wpi, partner): """Build a WarenpostInt.Address object from an Odoo partner object.""" def trim_phone(ph): if not ph: return None if len(ph) <= 15: return ph ph = ph.replace('-','') ph = ph.replace(' ','') return ph wpi_addr_lines = [] if partner.is_company: wpi_name = partner.name else: if partner.parent_id.name: wpi_name = partner.parent_id.name if partner.name: wpi_addr_lines.append(partner.name) else: wpi_name = partner.name wpi_addr_lines.append(partner.street) if partner.street2: wpi_addr_lines.append(partner.street2) wpi_phone = trim_phone(partner.phone) wpi_fax = trim_phone(partner.fax) wpi_state = partner.state_id.name if partner.state_id else None return wpi.Address(wpi_name, wpi_addr_lines, partner.city, partner.country_id.code, partner.zip, wpi_state, wpi_phone, wpi_fax, partner.email) def build_wpi_content_item(self, wpi, line): """Build contentPiece from Odoo 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 ptmpl = line.product_tmpl_id if product: 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 else: 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 hts = ptmpl.customs_code.strip() desc = ptmpl.customs_description_en if line.procurement_id and line.procurement_id.sale_line_id: price_unit = line.procurement_id.sale_line_id.price_unit else: raise Warning('Line has no procurement or procurement no sale order line?!?') weight_g = weight * 1000 line_value = q * price_unit return wpi.build_content_item(weight_g, line_value, q, hts, orig, desc) def build_wpi_content(self, wpi, picking): """Build contentPieces from Odoo stock.picking.""" content = [self.build_wpi_content_item(wpi, x) for x in picking.move_lines] total = 0.0 for i in content: total += float(i['contentPieceValue']) return (content, total) @api.one def wpi_send_shipping(self, pickings): config = self._get_config() order = self.env['sale.order'].search([('name','=',pickings.origin)]) recipient = pickings.partner_id warehouse = pickings.picking_type_id.warehouse_id.partner_id # determine weight and DP service/product weight = self._get_weight(order, pickings) service = self.get_service_by_class(recipient, weight, self.dp_service_class) if not service: raise Warning("Service not available for weight!") # connect to API wpi = self.conn_auth_wpi() # build various data structures for the API wpi_recipient = self.build_wpi_addr(wpi, recipient) wpi_sender = self.build_wpi_addr(wpi, warehouse) if self._country_code_outside_eu(recipient.country_id.code): (wpi_content, total_value) = self.build_wpi_content(wpi, pickings) else: wpi_content = [] total_value = 0 wpi_item = wpi.build_item(service.code, wpi_sender, wpi_recipient, weight*1000, total_value, 'EUR', customer_reference=pickings.name, contents=wpi_content) # actually create the order + download the label wpi_res = wpi.api_create_order([wpi_item], 'Max Mustermann') wpi_res_item = wpi_res['shipments'][0]['items'][0] png = wpi.api_get_item_label(wpi_res_item['id'], 'image/png') # build result dict awb = wpi_res['shipments'][0]['awb'] voucher_id = wpi_res_item['voucherId'] filename = 'WPI'+voucher_id+'.png' tracking_nr = ' ' if 'barcode' in wpi_res_item: tracking_nr += wpi_res_item['barcode'] result = { 'exact_price': service.cost_price, 'weight': service.weight, 'date_delivery': None, 'tracking_number': tracking_nr, 'voucher_id' : voucher_id, 'order_id' : awb, 'attachments': [(filename, png)]} _logger.debug(result) return result def _get_eu_res_country_group(self): eu_group = self.env.ref("base.europe", raise_if_not_found=False) if not eu_group: raise Warning(_('The Europe country group cannot be found. ' 'Please update the base module.')) return eu_group def get_services_by_country(self, service_class, country_code): if country_code == 'DE': return service_class.services_natl else: eu_country_group = self._get_eu_res_country_group() country_id = self.env['res.country'].search([('code','=',country_code)]) if country_id.id in eu_country_group.country_ids.ids: return service_class.services_eu else: return service_class.services_intl def _country_code_outside_eu(self, country_code): """Is the specified two-digit country code outside the EU?""" if country_code == 'DE': return False eu_country_group = self._get_eu_res_country_group() country_id = self.env['res.country'].search([('code','=',country_code)]) if country_id.id in eu_country_group.country_ids.ids: return False return True # determine lowest-matching-max-weight service within same class def get_service_by_class(self, recipient, weight, service_class): services = self.get_services_by_country(service_class, recipient.country_id.code) lowest_max_weight = 100000 lowest_service = None for s in services: if s.weight >= weight and s.weight < lowest_max_weight: lowest_max_weight = s.weight lowest_service = s return lowest_service # determine the maximum weight (in kg) of any service in this class def get_class_max_weight(self, service_class): services = self.get_services_by_country(service_class, recipient.country_id.code) highest_weight = 0 for s in services: if highest_weight > hightest_weight: highest_weight = s.weight return highest_weight @api.one def dp_send_shipping(self, pickings): if self.dp_service_class.is_wpi: return self.wpi_send_shipping(pickings)[0] config = self._get_config() order = self.env['sale.order'].search([('name','=',pickings.origin)]) recipient = pickings.partner_id warehouse = pickings.picking_type_id.warehouse_id.partner_id weight = self._get_weight(order, pickings) service = self.get_service_by_class(recipient, weight, self.dp_service_class) if not service: raise Warning("Service not available for weight!") im = self.conn_auth_im() im_recipient = self.build_im_addr(im, recipient) im_sender = self.build_im_addr(im, warehouse) im.clear_positions() position = im.build_position(service.code, im_sender, im_recipient) im.add_position(position) if im.wallet_balance < im.compute_total(): raise Warning("Wallet balance %f is less than label cost %f!" % (im.wallet_balance/100, im.compute_total()/100)) r = im.checkoutPNG() voucher = r.shoppingCart.voucherList.voucher[0] filename = 'DP'+voucher.voucherId+'.png' tracking_nr = ' ' if voucher.trackId: tracking_nr += voucher.trackId result = { 'exact_price': im.compute_total()/100, 'weight': service.weight, 'date_delivery': None, 'tracking_number': tracking_nr, 'voucher_id' : voucher.voucherId, 'order_id' : r.shoppingCart.shopOrderId, 'wallet_balance': r.walletBallance, 'attachments': [(filename, voucher.png_bin)]} return result def dp_get_shipping_price_from_so(self, order): price = 0 config = self._get_config() recipient = order.partner_shipping_id if order.partner_shipping_id else order.partner_id warehouse = order.warehouse_id.partner_id service_class = self.dp_service_class # single-package implementation weight = self._get_weight(order) service = self.get_service_by_class(recipient, weight, service_class) if not service: raise Warning("Service not available for weight!") return service.cost_price # compute the maximum weight of any service within class #class_max_weight = self.get_class_max_weight(service_class) # compute number of packages and each weight #weight, weight_limit, last_package, limits = self.get_package_count(class_max_weight, order) # iterate over list of packages #for line in range(1, limits+1): # if last_package and line == limits: # weight_limit = last_package # service = self.get_service_by_class(recipient, weight, service_class) # price += service.cost_price #return price @api.one def dp_get_tracking_link(self, pickings): return TRACKING_URL @api.one def dp_cancel_shipment(self, pickings): raise Warning('Cancelling DP Shipments not supported!')