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