Odoo integration of Deutsche Post Internetmarke Online Franking
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 lines
14 KiB

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!')