From a919aaf50fab67fc5963e44fd7d433feee7db706 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 29 Oct 2020 13:39:10 -0700 Subject: [PATCH] [MOV] delivery_gls_nl: from hibou-suite-enterprise:12.0 --- delivery_gls_nl/__init__.py | 1 + delivery_gls_nl/__manifest__.py | 26 ++ delivery_gls_nl/models/__init__.py | 1 + delivery_gls_nl/models/delivery_gls_nl.py | 294 ++++++++++++++++++ delivery_gls_nl/models/gls_nl_request.py | 36 +++ .../views/delivery_gls_nl_view.xml | 61 ++++ 6 files changed, 419 insertions(+) create mode 100644 delivery_gls_nl/__init__.py create mode 100644 delivery_gls_nl/__manifest__.py create mode 100644 delivery_gls_nl/models/__init__.py create mode 100644 delivery_gls_nl/models/delivery_gls_nl.py create mode 100644 delivery_gls_nl/models/gls_nl_request.py create mode 100644 delivery_gls_nl/views/delivery_gls_nl_view.xml diff --git a/delivery_gls_nl/__init__.py b/delivery_gls_nl/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_gls_nl/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_gls_nl/__manifest__.py b/delivery_gls_nl/__manifest__.py new file mode 100644 index 00000000..1b27d305 --- /dev/null +++ b/delivery_gls_nl/__manifest__.py @@ -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, +} diff --git a/delivery_gls_nl/models/__init__.py b/delivery_gls_nl/models/__init__.py new file mode 100644 index 00000000..d16621df --- /dev/null +++ b/delivery_gls_nl/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_gls_nl diff --git a/delivery_gls_nl/models/delivery_gls_nl.py b/delivery_gls_nl/models/delivery_gls_nl.py new file mode 100644 index 00000000..46fcebff --- /dev/null +++ b/delivery_gls_nl/models/delivery_gls_nl.py @@ -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
" + "Tracking Number: %s
" + "UniqueNo: %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) diff --git a/delivery_gls_nl/models/gls_nl_request.py b/delivery_gls_nl/models/gls_nl_request.py new file mode 100644 index 00000000..9011ed69 --- /dev/null +++ b/delivery_gls_nl/models/gls_nl_request.py @@ -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) diff --git a/delivery_gls_nl/views/delivery_gls_nl_view.xml b/delivery_gls_nl/views/delivery_gls_nl_view.xml new file mode 100644 index 00000000..8a928d17 --- /dev/null +++ b/delivery_gls_nl/views/delivery_gls_nl_view.xml @@ -0,0 +1,61 @@ + + + + + delivery.carrier.form.provider.gls_nl + delivery.carrier + + + + + + + + + + +