2016-07-13 14:11:42 +00:00
|
|
|
from openerp import api, fields, models
|
|
|
|
import logging
|
|
|
|
from openerp.exceptions import Warning
|
|
|
|
import pycountry
|
|
|
|
from inema import Internetmarke
|
2021-03-07 11:59:33 +00:00
|
|
|
from inema import WarenpostInt
|
2016-07-13 14:11:42 +00:00
|
|
|
|
|
|
|
_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):
|
2016-07-15 14:48:12 +00:00
|
|
|
# first try to split at last space
|
2016-07-13 14:11:42 +00:00
|
|
|
r = streethouse.rsplit(' ', 1)
|
2016-07-15 14:48:12 +00:00
|
|
|
# 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, '')
|
2016-07-13 14:11:42 +00:00
|
|
|
return (r[0], r[1])
|
|
|
|
|
|
|
|
def split_first_lastname(name):
|
2016-07-15 14:48:12 +00:00
|
|
|
# try to split at last space
|
2016-07-13 14:11:42 +00:00
|
|
|
r = name.rsplit(' ', 1)
|
2016-07-15 14:48:12 +00:00
|
|
|
# if this fails, simply claim everything is the last name
|
|
|
|
if len(r) < 2:
|
|
|
|
return ("", name)
|
2016-07-13 14:11:42 +00:00
|
|
|
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)
|
2016-07-15 14:48:49 +00:00
|
|
|
street2 = None
|
|
|
|
if partner.street2:
|
|
|
|
street2 = partner.street2
|
2017-05-14 21:33:30 +00:00
|
|
|
# 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
|
2016-07-13 14:11:42 +00:00
|
|
|
addr = im.build_addr(street = street,
|
|
|
|
house = house,
|
2016-07-15 14:48:49 +00:00
|
|
|
additional = street2,
|
2017-05-14 21:33:30 +00:00
|
|
|
zipcode = zipcode,
|
2016-07-13 14:11:42 +00:00
|
|
|
city = partner.city,
|
|
|
|
country = country)
|
2016-07-15 15:03:20 +00:00
|
|
|
if partner.is_company:
|
2016-07-15 18:28:58 +00:00
|
|
|
return im.build_comp_addr(company = partner.name,
|
2016-07-15 15:03:20 +00:00
|
|
|
address = addr)
|
|
|
|
else:
|
|
|
|
if partner.parent_id.name:
|
|
|
|
person = None
|
|
|
|
if partner.name:
|
2016-12-13 13:17:55 +00:00
|
|
|
(first, last) = split_first_lastname(partner.name)
|
2016-12-13 13:31:35 +00:00
|
|
|
title = None
|
|
|
|
if partner.title and partner.title.shortcut:
|
|
|
|
title = partner.title.shortcut
|
|
|
|
person = im.build_pers_name(first=first, last=last, title=title)
|
2016-07-15 15:03:20 +00:00
|
|
|
return im.build_comp_addr(company = partner.parent_id.name,
|
|
|
|
address = addr,
|
|
|
|
person = person)
|
2016-07-15 15:10:43 +00:00
|
|
|
else:
|
|
|
|
(first, last) = split_first_lastname(partner.name)
|
|
|
|
return im.build_pers_addr(first = first,
|
|
|
|
last = last,
|
|
|
|
address = addr)
|
2016-07-13 14:11:42 +00:00
|
|
|
|
2021-03-07 11:59:33 +00:00
|
|
|
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']
|
|
|
|
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
|
2021-03-07 22:14:57 +00:00
|
|
|
ptmpl = line.product_tmpl_id
|
2021-03-07 11:59:33 +00:00
|
|
|
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
|
2021-03-07 22:14:57 +00:00
|
|
|
else:
|
2021-03-07 11:59:33 +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))
|
|
|
|
weight = ptempl.weight
|
2021-03-07 22:14:57 +00:00
|
|
|
hts = ptmpl.customs_code.strip()
|
|
|
|
desc = ptmpl.customs_description_en
|
2021-03-07 11:59:33 +00:00
|
|
|
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
|
2021-03-07 22:14:57 +00:00
|
|
|
return wpi.build_content_item(weight_g, line_value, q, hts, orig, desc)
|
2021-03-07 11:59:33 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-03-07 11:28:47 +00:00
|
|
|
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
|
|
|
|
|
2016-07-15 18:27:56 +00:00
|
|
|
def get_services_by_country(self, service_class, country_code):
|
|
|
|
if country_code == 'DE':
|
|
|
|
return service_class.services_natl
|
|
|
|
else:
|
2021-03-07 11:28:47 +00:00
|
|
|
eu_country_group = self._get_eu_res_country_group()
|
|
|
|
country_id = self.env['res.country'].search([('code','=',country_code)])
|
2021-03-07 11:59:33 +00:00
|
|
|
if country_id.id in eu_country_group.country_ids.ids:
|
2021-03-07 11:28:47 +00:00
|
|
|
return service_class.services_eu
|
|
|
|
else:
|
|
|
|
return service_class.services_intl
|
2016-07-15 18:27:56 +00:00
|
|
|
|
2021-03-07 11:59:33 +00:00
|
|
|
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
|
|
|
|
|
2016-07-15 18:27:56 +00:00
|
|
|
# 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
|
|
|
|
|
2016-07-13 14:11:42 +00:00
|
|
|
@api.one
|
|
|
|
def dp_send_shipping(self, pickings):
|
2021-03-07 11:59:33 +00:00
|
|
|
if self.dp_service_class.is_wpi:
|
|
|
|
return self.wpi_send_shipping(pickings)[0]
|
2016-07-13 14:11:42 +00:00
|
|
|
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
|
2016-07-15 18:27:56 +00:00
|
|
|
weight = self._get_weight(order, pickings)
|
2021-03-07 11:59:16 +00:00
|
|
|
service = self.get_service_by_class(recipient, weight, self.dp_service_class)
|
2016-07-15 18:27:56 +00:00
|
|
|
if not service:
|
|
|
|
raise Warning("Service not available for weight!")
|
2016-07-13 14:11:42 +00:00
|
|
|
im = self.conn_auth_im()
|
|
|
|
im_recipient = self.build_im_addr(im, recipient)
|
2016-07-15 14:49:20 +00:00
|
|
|
im_sender = self.build_im_addr(im, warehouse)
|
2016-07-15 18:27:56 +00:00
|
|
|
im.clear_positions()
|
2016-07-15 14:49:20 +00:00
|
|
|
position = im.build_position(service.code, im_sender, im_recipient)
|
2016-07-13 14:11:42 +00:00
|
|
|
im.add_position(position)
|
2017-01-10 14:26:50 +00:00
|
|
|
if im.wallet_balance < im.compute_total():
|
2017-01-10 14:30:04 +00:00
|
|
|
raise Warning("Wallet balance %f is less than label cost %f!" % (im.wallet_balance/100, im.compute_total()/100))
|
2016-07-13 14:11:42 +00:00
|
|
|
r = im.checkoutPNG()
|
|
|
|
voucher = r.shoppingCart.voucherList.voucher[0]
|
2016-07-15 15:10:54 +00:00
|
|
|
filename = 'DP'+voucher.voucherId+'.png'
|
2016-07-15 14:49:34 +00:00
|
|
|
tracking_nr = ' '
|
|
|
|
if voucher.trackId:
|
|
|
|
tracking_nr += voucher.trackId
|
|
|
|
result = { 'exact_price': im.compute_total()/100,
|
2016-07-13 14:11:42 +00:00
|
|
|
'weight': service.weight,
|
|
|
|
'date_delivery': None,
|
2016-07-15 14:49:34 +00:00
|
|
|
'tracking_number': tracking_nr,
|
2016-07-13 14:11:42 +00:00
|
|
|
'voucher_id' : voucher.voucherId,
|
|
|
|
'order_id' : r.shoppingCart.shopOrderId,
|
2016-07-15 15:16:40 +00:00
|
|
|
'wallet_balance': r.walletBallance,
|
2016-07-15 14:49:20 +00:00
|
|
|
'attachments': [(filename, voucher.png_bin)]}
|
2016-07-13 14:11:42 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
def dp_get_shipping_price_from_so(self, order):
|
2016-07-15 18:27:56 +00:00
|
|
|
price = 0
|
2016-07-13 14:11:42 +00:00
|
|
|
config = self._get_config()
|
|
|
|
recipient = order.partner_shipping_id if order.partner_shipping_id else order.partner_id
|
|
|
|
warehouse = order.warehouse_id.partner_id
|
2021-03-07 11:59:16 +00:00
|
|
|
service_class = self.dp_service_class
|
2016-07-15 18:27:56 +00:00
|
|
|
|
|
|
|
# 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!")
|
2016-07-13 14:11:42 +00:00
|
|
|
return service.cost_price
|
|
|
|
|
2016-07-15 18:27:56 +00:00
|
|
|
# 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
|
|
|
|
|
2016-07-13 14:11:42 +00:00
|
|
|
@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!')
|