mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[MOV] delivery_gls_nl: from hibou-suite-enterprise:12.0
This commit is contained in:
1
delivery_gls_nl/__init__.py
Normal file
1
delivery_gls_nl/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
26
delivery_gls_nl/__manifest__.py
Normal file
26
delivery_gls_nl/__manifest__.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
'name': 'GLS Netherlands Shipping',
|
||||||
|
'summary': 'Create and print your shipping labels with GLS from the Netherlands.',
|
||||||
|
'version': '12.0.1.0.0',
|
||||||
|
'author': "Hibou Corp.",
|
||||||
|
'category': 'Warehouse',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
GLS Netherlands Shipping
|
||||||
|
========================
|
||||||
|
|
||||||
|
Create and print your shipping labels with GLS from the Netherlands.
|
||||||
|
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'delivery_hibou',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
'views/delivery_gls_nl_view.xml',
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
1
delivery_gls_nl/models/__init__.py
Normal file
1
delivery_gls_nl/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import delivery_gls_nl
|
||||||
294
delivery_gls_nl/models/delivery_gls_nl.py
Normal file
294
delivery_gls_nl/models/delivery_gls_nl.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from .gls_nl_request import GLSNLRequest
|
||||||
|
from requests import HTTPError
|
||||||
|
from base64 import decodebytes
|
||||||
|
from csv import reader as csv_reader
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPackaging(models.Model):
|
||||||
|
_inherit = 'product.packaging'
|
||||||
|
|
||||||
|
package_carrier_type = fields.Selection(selection_add=[('gls_nl', 'GLS Netherlands')])
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderGLSNL(models.Model):
|
||||||
|
_inherit = 'delivery.carrier'
|
||||||
|
|
||||||
|
GLS_NL_SOFTWARE_NAME = 'Odoo'
|
||||||
|
GLS_NL_SOFTWARE_VER = '12.0'
|
||||||
|
GLS_NL_COUNTRY_NOT_FOUND = 'GLS_NL_COUNTRY_NOT_FOUND'
|
||||||
|
|
||||||
|
delivery_type = fields.Selection(selection_add=[('gls_nl', 'GLS Netherlands')])
|
||||||
|
|
||||||
|
gls_nl_username = fields.Char(string='GLS NL Username', groups='base.group_system')
|
||||||
|
gls_nl_password = fields.Char(string='GLS NL Password', groups='base.group_system')
|
||||||
|
gls_nl_labeltype = fields.Selection([
|
||||||
|
('zpl', 'ZPL'),
|
||||||
|
('pdf', 'PDF'),
|
||||||
|
], string='GLS NL Label Type')
|
||||||
|
gls_nl_express = fields.Selection([
|
||||||
|
('t9', 'Delivery before 09:00 on weekdays'),
|
||||||
|
('t12', 'Delivery before 12:00 on weekdays'),
|
||||||
|
('t17', 'Delivery before 17:00 on weekdays'),
|
||||||
|
('s9', 'Delivery before 09:00 on Saturday'),
|
||||||
|
('s12', 'Delivery before 12:00 on Saturday'),
|
||||||
|
('s17', 'Delivery before 17:00 on Saturday'),
|
||||||
|
], string='GLS NL Express', help='Express service tier (leave blank for regular)')
|
||||||
|
gls_nl_rate_id = fields.Many2one('ir.attachment', string='GLS NL Rates')
|
||||||
|
|
||||||
|
def button_gls_nl_test_rates(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.gls_nl_rate_id:
|
||||||
|
raise UserError(_('No GLS NL Rate file is attached.'))
|
||||||
|
rate_data = self._gls_nl_process_rates()
|
||||||
|
weight_col_count = len(rate_data['w'])
|
||||||
|
row_count = len(rate_data['r'])
|
||||||
|
country_col = rate_data['c']
|
||||||
|
country_model = self.env['res.country']
|
||||||
|
for row in rate_data['r']:
|
||||||
|
country = country_model.search([('code', '=', row[country_col])], limit=1)
|
||||||
|
if not country:
|
||||||
|
raise ValidationError(_('Could not locate country by code: "%s" for row: %s') % (row[country_col], row))
|
||||||
|
for w, i in rate_data['w'].items():
|
||||||
|
try:
|
||||||
|
cost = float(row[i])
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(_('Could not process cost for row: %s') % (row, ))
|
||||||
|
raise ValidationError(_('Looks good! %s weights, %s countries located.') % (weight_col_count, row_count))
|
||||||
|
|
||||||
|
def _gls_nl_process_rates(self):
|
||||||
|
"""
|
||||||
|
'w' key will be weights to row index map
|
||||||
|
'c' key will be the country code index
|
||||||
|
'r' key will be rows from the original that can use indexes above
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
datab = decodebytes(self.gls_nl_rate_id.datas)
|
||||||
|
csv_data = datab.decode()
|
||||||
|
csv_data = csv_data.replace('\r', '')
|
||||||
|
csv_lines = csv_data.splitlines()
|
||||||
|
header = [csv_lines[0]]
|
||||||
|
body = csv_lines[1:]
|
||||||
|
data = {'w': {}, 'r': []}
|
||||||
|
for row in csv_reader(header):
|
||||||
|
for i, col in enumerate(row):
|
||||||
|
if col == 'Country':
|
||||||
|
data['c'] = i
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
weight = float(col)
|
||||||
|
data['w'][weight] = i
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if 'c' not in data:
|
||||||
|
raise ValidationError(_('Could not locate "Country" column.'))
|
||||||
|
if not data['w']:
|
||||||
|
raise ValidationError(_('Could not locate any weight columns.'))
|
||||||
|
for row in csv_reader(body):
|
||||||
|
data['r'].append(row)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _gls_nl_rate(self, country_code, weight):
|
||||||
|
if weight < 0.0:
|
||||||
|
return 0.0
|
||||||
|
rate_data = self._gls_nl_process_rates()
|
||||||
|
country_col = rate_data['c']
|
||||||
|
rate = None
|
||||||
|
country_found = False
|
||||||
|
for row in rate_data['r']:
|
||||||
|
if row[country_col] == country_code:
|
||||||
|
country_found = True
|
||||||
|
for w, i in rate_data['w'].items():
|
||||||
|
if weight <= w:
|
||||||
|
try:
|
||||||
|
rate = float(row[i])
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# our w, i will be the last weight and rate.
|
||||||
|
try:
|
||||||
|
# Return Max rate + remaining weight rated
|
||||||
|
return float(row[i]) + self._gls_nl_rate(country_code, weight-w)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
if rate is None and not country_found:
|
||||||
|
return self.GLS_NL_COUNTRY_NOT_FOUND
|
||||||
|
return rate
|
||||||
|
|
||||||
|
def gls_nl_rate_shipment(self, order):
|
||||||
|
recipient = self.get_recipient(order=order)
|
||||||
|
rate = None
|
||||||
|
dest_country = recipient.country_id.code
|
||||||
|
est_weight_value = sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0
|
||||||
|
if dest_country:
|
||||||
|
rate = self._gls_nl_rate(dest_country, est_weight_value)
|
||||||
|
|
||||||
|
# Handle errors and rate conversions.
|
||||||
|
error_message = None
|
||||||
|
if not dest_country or rate == self.GLS_NL_COUNTRY_NOT_FOUND:
|
||||||
|
error_message = _('Destination country not found: "%s"') % (dest_country, )
|
||||||
|
if rate is None or error_message:
|
||||||
|
if not error_message:
|
||||||
|
error_message = _('Rate not found for weight: "%s"') % (est_weight_value, )
|
||||||
|
return {'success': False,
|
||||||
|
'price': 0.0,
|
||||||
|
'error_message': error_message,
|
||||||
|
'warning_message': False}
|
||||||
|
|
||||||
|
euro_currency = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1)
|
||||||
|
if euro_currency and order.currency_id and euro_currency != order.currency_id:
|
||||||
|
rate = euro_currency._convert(rate,
|
||||||
|
order.currency_id,
|
||||||
|
order.company_id,
|
||||||
|
order.date_order or fields.Date.today())
|
||||||
|
|
||||||
|
return {'success': True,
|
||||||
|
'price': rate,
|
||||||
|
'error_message': False,
|
||||||
|
'warning_message': False}
|
||||||
|
|
||||||
|
def _get_gls_nl_service(self):
|
||||||
|
return GLSNLRequest(self.prod_environment)
|
||||||
|
|
||||||
|
def _gls_nl_make_address(self, partner):
|
||||||
|
# Addresses look like
|
||||||
|
# {
|
||||||
|
# 'name1': '',
|
||||||
|
# 'name2': '',
|
||||||
|
# 'name3': '',
|
||||||
|
# 'street': '',
|
||||||
|
# 'houseNo': '',
|
||||||
|
# 'houseNoExt': '',
|
||||||
|
# 'zipCode': '',
|
||||||
|
# 'city': '',
|
||||||
|
# 'countrycode': '',
|
||||||
|
# 'contact': '',
|
||||||
|
# 'phone': '',
|
||||||
|
# 'email': '',
|
||||||
|
# }
|
||||||
|
address = {}
|
||||||
|
pieces = partner.street.split(' ')
|
||||||
|
street = ' '.join(pieces[:-1]).strip(' ,')
|
||||||
|
house = pieces[-1]
|
||||||
|
address['name1'] = partner.name
|
||||||
|
address['street'] = street
|
||||||
|
address['houseNo'] = house
|
||||||
|
if partner.street2:
|
||||||
|
address['houseNoExt'] = partner.street2
|
||||||
|
address['zipCode'] = partner.zip
|
||||||
|
address['city'] = partner.city
|
||||||
|
address['countrycode'] = partner.country_id.code
|
||||||
|
if partner.phone:
|
||||||
|
address['phone'] = partner.phone
|
||||||
|
if partner.email:
|
||||||
|
address['email'] = partner.email
|
||||||
|
return address
|
||||||
|
|
||||||
|
def gls_nl_send_shipping(self, pickings):
|
||||||
|
res = []
|
||||||
|
sudoself = self.sudo()
|
||||||
|
service = sudoself._get_gls_nl_service()
|
||||||
|
|
||||||
|
for picking in pickings:
|
||||||
|
#company = self.get_shipper_company(picking=picking) # Requester not needed currently
|
||||||
|
from_ = self.get_shipper_warehouse(picking=picking)
|
||||||
|
to = self.get_recipient(picking=picking)
|
||||||
|
total_rate = 0.0
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
'labelType': sudoself.gls_nl_labeltype,
|
||||||
|
'username': sudoself.gls_nl_username,
|
||||||
|
'password': sudoself.gls_nl_password,
|
||||||
|
'shiptype': 'p', # note not shipType, 'f' for Freight
|
||||||
|
'trackingLinkType': 's',
|
||||||
|
# 'customerNo': '', # needed if there are more 'customer numbers' attached to a single MyGLS API Account
|
||||||
|
'reference': picking.name,
|
||||||
|
'addresses': {
|
||||||
|
'pickupAddress': self._gls_nl_make_address(from_),
|
||||||
|
'deliveryAddress': self._gls_nl_make_address(to),
|
||||||
|
#'requesterAddress': {}, # Not needed currently
|
||||||
|
},
|
||||||
|
'units': [],
|
||||||
|
'services': {},
|
||||||
|
'shippingDate': fields.Date.to_string(fields.Date.today()),
|
||||||
|
'shippingSystemName': self.GLS_NL_SOFTWARE_NAME,
|
||||||
|
'shippingSystemVersion': self.GLS_NL_SOFTWARE_VER,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sudoself.gls_nl_express:
|
||||||
|
request_body['services']['expressService'] = sudoself.gls_nl_express
|
||||||
|
|
||||||
|
# Build out units
|
||||||
|
# Units look like:
|
||||||
|
# {
|
||||||
|
# 'unitId': 'A',
|
||||||
|
# 'unitType': '', # only for freight
|
||||||
|
# 'weight': 0.0,
|
||||||
|
# 'additionalInfo1': '',
|
||||||
|
# 'additionalInfo2': '',
|
||||||
|
# }
|
||||||
|
if picking.package_ids:
|
||||||
|
for package in picking.package_ids:
|
||||||
|
rate = self._gls_nl_rate(to.country_id.code, package.shipping_weight or 0.0)
|
||||||
|
if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND:
|
||||||
|
total_rate += rate
|
||||||
|
unit = {
|
||||||
|
'unitId': package.name,
|
||||||
|
'weight': package.shipping_weight,
|
||||||
|
}
|
||||||
|
request_body['units'].append(unit)
|
||||||
|
else:
|
||||||
|
rate = self._gls_nl_rate(to.country_id.code, picking.shipping_weight or 0.0)
|
||||||
|
if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND:
|
||||||
|
total_rate += rate
|
||||||
|
unit = {
|
||||||
|
'unitId': picking.name,
|
||||||
|
'weight': picking.shipping_weight,
|
||||||
|
}
|
||||||
|
request_body['units'].append(unit)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create label
|
||||||
|
label = service.create_label(request_body)
|
||||||
|
trackings = []
|
||||||
|
uniq_nos = []
|
||||||
|
attachments = []
|
||||||
|
for i, unit in enumerate(label['units'], 1):
|
||||||
|
trackings.append(unit['unitNo'])
|
||||||
|
uniq_nos.append(unit['uniqueNo'])
|
||||||
|
attachments.append(('LabelGLSNL-%s-%s.%s' % (unit['unitNo'], i, sudoself.gls_nl_labeltype), unit['label']))
|
||||||
|
|
||||||
|
tracking = ', '.join(set(trackings))
|
||||||
|
logmessage = _("Shipment created into GLS NL<br/>"
|
||||||
|
"<b>Tracking Number:</b> %s<br/>"
|
||||||
|
"<b>UniqueNo:</b> %s") % (tracking, ', '.join(set(uniq_nos)))
|
||||||
|
picking.message_post(body=logmessage, attachments=attachments)
|
||||||
|
shipping_data = {'exact_price': total_rate, 'tracking_number': tracking}
|
||||||
|
res.append(shipping_data)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise ValidationError(e)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def gls_nl_get_tracking_link(self, pickings):
|
||||||
|
return 'https://gls-group.eu/EU/en/parcel-tracking?match=%s' % pickings.carrier_tracking_ref
|
||||||
|
|
||||||
|
def gls_nl_cancel_shipment(self, picking):
|
||||||
|
sudoself = self.sudo()
|
||||||
|
service = sudoself._get_gls_nl_service()
|
||||||
|
try:
|
||||||
|
request_body = {
|
||||||
|
'unitNo': picking.carrier_tracking_ref,
|
||||||
|
'username': sudoself.gls_nl_username,
|
||||||
|
'password': sudoself.gls_nl_password,
|
||||||
|
'shiptype': 'p',
|
||||||
|
'shippingSystemName': self.GLS_NL_SOFTWARE_NAME,
|
||||||
|
'shippingSystemVersion': self.GLS_NL_SOFTWARE_VER,
|
||||||
|
}
|
||||||
|
service.delete_label(request_body)
|
||||||
|
picking.message_post(body=_('Shipment N° %s has been cancelled' % picking.carrier_tracking_ref))
|
||||||
|
picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0})
|
||||||
|
except HTTPError as e:
|
||||||
|
raise ValidationError(e)
|
||||||
36
delivery_gls_nl/models/gls_nl_request.py
Normal file
36
delivery_gls_nl/models/gls_nl_request.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import requests
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
|
class GLSNLRequest:
|
||||||
|
def __init__(self, production):
|
||||||
|
self.production = production
|
||||||
|
self.api_key = '234a6d4ad5fd4d039526a8a1074051ee' if production else 'f80d41c6f7d542878c9c0a4295de7a6a'
|
||||||
|
self.url = 'https://api.gls.nl/V1/api' if production else 'https://api.gls.nl/Test/V1/api'
|
||||||
|
self.headers = self._make_headers()
|
||||||
|
|
||||||
|
def _make_headers(self):
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Ocp-Apim-Subscription-Key': self.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
def post_request(self, endpoint, body):
|
||||||
|
if not self.production and body.get('username') == 'test':
|
||||||
|
# Override to test credentials
|
||||||
|
body['username'] = 'apitest1@gls-netherlands.com'
|
||||||
|
body['password'] = '9PMev9qM'
|
||||||
|
url = self.url + endpoint
|
||||||
|
result = requests.request('POST', url, headers=self.headers, data=dumps(body))
|
||||||
|
if result.status_code != 200:
|
||||||
|
raise requests.HTTPError(result.text)
|
||||||
|
return result.json()
|
||||||
|
|
||||||
|
def create_label(self, body):
|
||||||
|
return self.post_request('/Label/Create', body)
|
||||||
|
|
||||||
|
def confirm_label(self, body):
|
||||||
|
return self.post_request('/Label/Confirm', body)
|
||||||
|
|
||||||
|
def delete_label(self, body):
|
||||||
|
return self.post_request('/Label/Delete', body)
|
||||||
61
delivery_gls_nl/views/delivery_gls_nl_view.xml
Normal file
61
delivery_gls_nl/views/delivery_gls_nl_view.xml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_delivery_carrier_form_with_provider_gls_nl" model="ir.ui.view">
|
||||||
|
<field name="name">delivery.carrier.form.provider.gls_nl</field>
|
||||||
|
<field name="model">delivery.carrier</field>
|
||||||
|
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='destination']" position='before'>
|
||||||
|
<page string="GLS NL Configuration" attrs="{'invisible': [('delivery_type', '!=', 'gls_nl')]}">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="gls_nl_username" attrs="{'required': [('delivery_type', '=', 'gls_nl')]}" />
|
||||||
|
<field name="gls_nl_password" attrs="{'required': [('delivery_type', '=', 'gls_nl')]}" password="True"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<button name="button_gls_nl_test_rates" type="object" string="Test Rates" class="bnt-primary" attrs="{'invisible': [('gls_nl_rate_id', '=', False)]}"/>
|
||||||
|
<field name="gls_nl_rate_id"/>
|
||||||
|
<field name="gls_nl_labeltype" attrs="{'required': [('delivery_type', '==', 'gls_nl')]}"/>
|
||||||
|
<field name="gls_nl_express"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string='GLS NL Tutorial'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Go to <a href='https://api-portal.gls.nl/' target='_blank'>GLS Netherlands API</a> to create an API capable account.</b>
|
||||||
|
<br/><br/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>You can use the username of "test" and any password to use default test credentials (only when not in Production mode).
|
||||||
|
This allows you to put in your production password and simply replace the username with "test" to use the API default test credentials.<br/>
|
||||||
|
(as of 2019-07-28 it is known that actual credentials will not work on the test environment, and the test environment is not available at all times of the day)</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Rates can be uploaded with the following format.</b>
|
||||||
|
<ul>
|
||||||
|
<li>Header row with One Column named "Country". Rows should have 2 letter country codes in this column.</li>
|
||||||
|
<li>Header row with numeric values in ascending value to the right (weight in kg). Rows should have the rate in Euros if the order/package is less than the header weight.</li>
|
||||||
|
<li>Columns not matching the mentioned headers will be ignored and can be used for comments or maintainer data.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Example:</p>
|
||||||
|
<pre>Country Name,Country,2,5,15,32
|
||||||
|
Belgium,BE,4.7,4.95,5.9,9.7
|
||||||
|
Germany,DE,4.15,5.3,6.4,12.8
|
||||||
|
</pre>
|
||||||
|
<p>Rating a package going to Germany weighing <b>4.5kg</b> (less than 5kg) will return <b>5.30 EUR</b> (converted to order currency).</p>
|
||||||
|
<p>It is recommended to name the attachment appropriately (e.g. <b>GLS_RATES_2019.csv</b>), and find/use the same attachment on other delivery methods (e.g. with express settings and higher margin).</p>
|
||||||
|
<br/><br/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Use the <b>Test Rates</b> button to perform a thorough test (weights parse as numeric for all rows, country is round in Odoo database).
|
||||||
|
<br/><br/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user