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: converted_weight = self._gls_convert_weight(package.shipping_weight) rate = self._gls_nl_rate(to.country_id.code, converted_weight or 0.0) if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND: total_rate += rate unit = { 'unitId': package.name, 'weight': converted_weight } request_body['units'].append(unit) else: converted_weight = self._gls_convert_weight(picking.shipping_weight) rate = self._gls_nl_rate(to.country_id.code, converted_weight or 0.0) if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND: total_rate += rate unit = { 'unitId': picking.name, 'weight': converted_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) def _gls_convert_weight(self, weight): get_param = self.env['ir.config_parameter'].sudo().get_param product_weight_in_lbs_param = get_param('product.weight_in_lbs') if product_weight_in_lbs_param == '1': return weight / 2.20462 else: return weight