From acf051f03ba982673e7d63ddc46d275be3406cfb Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 29 Oct 2020 11:43:43 -0700 Subject: [PATCH 1/9] [MOV] delivery_gso: from hibou-suite-enterprise:11.0 --- delivery_gso/__init__.py | 1 + delivery_gso/__manifest__.py | 25 ++ delivery_gso/models/__init__.py | 1 + delivery_gso/models/delivery_gso.py | 321 +++++++++++++++++++++++ delivery_gso/models/requests_gso.py | 47 ++++ delivery_gso/views/delivery_gso_view.xml | 28 ++ 6 files changed, 423 insertions(+) create mode 100644 delivery_gso/__init__.py create mode 100644 delivery_gso/__manifest__.py create mode 100644 delivery_gso/models/__init__.py create mode 100644 delivery_gso/models/delivery_gso.py create mode 100644 delivery_gso/models/requests_gso.py create mode 100644 delivery_gso/views/delivery_gso_view.xml diff --git a/delivery_gso/__init__.py b/delivery_gso/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_gso/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py new file mode 100644 index 00000000..3f94b3c9 --- /dev/null +++ b/delivery_gso/__manifest__.py @@ -0,0 +1,25 @@ +{ + 'name': 'Golden State Overnight (gso.com) Shipping', + 'summary': 'Send your shippings through gso.com and track them online.', + 'version': '11.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +Golden State Overnight (gso.com) Shipping +========================== +* Provides estimates on shipping costs through gso.com. +* Send your shippings through gso.com and allows tracking of packages. +""", + 'depends': [ + 'delivery_hibou', + ], + 'demo': [], + 'data': [ + 'views/delivery_gso_view.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/delivery_gso/models/__init__.py b/delivery_gso/models/__init__.py new file mode 100644 index 00000000..943392d3 --- /dev/null +++ b/delivery_gso/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_gso diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py new file mode 100644 index 00000000..84bb6477 --- /dev/null +++ b/delivery_gso/models/delivery_gso.py @@ -0,0 +1,321 @@ +import pytz +from math import ceil +from requests import HTTPError +from hashlib import sha1 + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError, UserError + +from .requests_gso import GSORequest + +GSO_TZ = 'PST8PDT' + + +class ProductPackaging(models.Model): + _inherit = 'product.packaging' + + package_carrier_type = fields.Selection(selection_add=[('gso', 'gso.com')]) + + +class ProviderGSO(models.Model): + _inherit = 'delivery.carrier' + + delivery_type = fields.Selection(selection_add=[('gso', 'gso.com')]) + gso_username = fields.Char(string='gso.com Username', groups='base.group_system') + gso_password = fields.Char(string='gso.com Password', groups='base.group_system') + gso_account_number = fields.Char(string='gso.com Account Number', groups='base.group_system') + gso_default_packaging_id = fields.Many2one('product.packaging', string='Default Package Type') + # For service type, SAM, SPM, and SEV require authorized accounts. + gso_service_type = fields.Selection([('PDS', 'Priority Overnight'), + ('EPS', 'Early Priority Overnight'), + ('NPS', 'Noon Priority Overnight'), + ('SDS', 'Saturday Delivery'), + ('ESS', 'Early Saturday Delivery'), + ('CPS', 'GSO Ground'), + ('SAM', 'AM Select (8A-12P) Delivery Window'), + ('SPM', 'PM Select (12P-4P) Delivery Window'), + ('SEV', 'Evening Select (4P-8P) Delivery Window'), + ], + string="Service Type", default="CPS", help="Service Type determines speed of delivery") + gso_image_type = fields.Selection([('NO_LABEL', 'No Label'), + ('PAPER_LABEL', 'Paper Label'), + ('ZPL_SHORT_LABEL', 'Short Label'), + ('ZPL_LONG_LABEL', 'Long label'), + ], + string="Image Type", default="ZPL_SHORT_LABEL", help="Image Type is the type of Label to use") + + def _get_gso_service(self): + return GSORequest(self.prod_environment, + self.gso_username, + self.gso_password, + self.gso_account_number) + + def _gso_make_ship_address(self, partner): + # Addresses look like + # { + # 'ShipToCompany': '', + # 'ShipToAttention': '', + # 'ShipToPhone': '', + # 'ShipToEmail': '', + # 'DeliveryAddress1': '', + # 'DeliveryAddress2': '', + # 'DeliveryCity': '', + # 'DeliveryState': '', + # 'DeliveryZip': '', + # } + address = {} + # ShipToCompany is required. ShipToAttention which is a person is not. + if partner.name and not partner.parent_id: + address['ShipToCompany'] = partner.name + if partner.name and partner.parent_id: + address['ShipToCompany'] = partner.parent_id.name # or partner.parent_id.id.name ?? + address['ShipToAttention'] = partner.name + + if partner.phone: + address['ShipToPhone'] = partner.phone + if partner.email: + address['ShipToEmail'] = partner.email + if partner.street: + address['DeliveryAddress1'] = partner.street + if partner.street2: + address['DeliveryAddress2'] = partner.street2 + if partner.city: + address['DeliveryCity'] = partner.city + if partner.state_id: + address['DeliveryState'] = partner.state_id.code + if partner.zip: + address['DeliveryZip'] = partner.zip + + return address + + def _gso_make_shipper_address(self, warehouse, company): + # Addresses look like + # { + # 'ShipperCompany': '', + # 'ShipperContact': '', + # 'ShipperPhone': '', + # 'ShipperEmail': '', + # 'PickupAddress1': '', + # 'PickupAddress2': '', + # 'PickupCity': '', + # 'PickupState': '', + # 'PickupZip': '', + # } + address = {} + if company.name and not company.parent_id: + address['ShipperCompany'] = company.name + if company.name and company.parent_id: + address['ShipperCompany'] = company.parent_id.name + address['ShipperContact'] = company.name + + if warehouse.phone: + address['ShipperPhone'] = warehouse.phone + if warehouse.email: + address['ShipperEmail'] = warehouse.email + if warehouse.street: + address['PickupAddress1'] = warehouse.street + if warehouse.street2: + address['PickupAddress2'] = warehouse.street2 + if warehouse.city: + address['PickupCity'] = warehouse.city + if warehouse.state_id: + address['PickupState'] = warehouse.state_id.code + if warehouse.zip: + address['PickupZip'] = warehouse.zip + + return address + + def _gso_create_tracking_number(self, identifier): + # Override for a more 'customized' tracking number + # Expects a self.sudo() + if not identifier: + identifier = fields.Datetime.now() # string in Odoo 11 + salt = self.env['ir.config_parameter'].sudo().get_param('database.secret') + sha = sha1((identifier + salt).encode()).hexdigest() + return sha[:20] + + def _gso_get_package_dimensions(self, package=None): + if not package: + package_type = self.gso_default_packaging_id + else: + package_type = package.packaging_id + return {'Length': package_type.length, 'Width': package_type.width, 'Height': package_type.height} + + def _gso_convert_weight(self, weight_in_kg): + # m(lb) = m(kg) / 0.45359237 + weight_in_lb = weight_in_kg / 0.45359237 + # If less than 8 oz... + if weight_in_lb < 0.5: + return 0 + else: + # Round up to nearest lb + return int(ceil(weight_in_lb)) + + def gso_send_shipping(self, pickings): + res = [] + sudoself = self.sudo() + service = sudoself._get_gso_service() + + for picking in pickings: + company = self.get_shipper_company(picking=picking) + from_ = self.get_shipper_warehouse(picking=picking) + to = self.get_recipient(picking=picking) + address_type = 'B' if bool(to.company or to.parent_id.company) else 'R' + + request_body = { + 'AccountNumber': sudoself.gso_account_number, + 'Shipment': { + 'ServiceCode': sudoself.gso_service_type, + 'ShipmentLabelType': sudoself.gso_image_type, + 'SignatureCode': 'SIG_NOT_REQD', + 'DeliveryAddressType': address_type, + # 'ShipDate': fields.Date.today(), # safer not to send in case you want to ship on a weekend + }, + } + request_body['Shipment'].update(self._gso_make_shipper_address(from_, company)) + request_body['Shipment'].update(self._gso_make_ship_address(to)) + + # Automatic insurance at $100.0 + insurance_value = sudoself.get_insurance_value(picking=picking) + if insurance_value: + request_body['Shipment']['SignatureCode'] = 'SIG_REQD' + if insurance_value > 100.0: + # Documentation says to set DeclaredValue ONLY if over $100.00 + request_body['Shipment']['DeclaredValue'] = insurance_value + + cost = 0.0 + labels = { + 'thermal': [], + 'paper': [], + } + if picking.package_ids: + # Every package will be a transaction + for package in picking.package_ids: + request_body['Shipment']['Weight'] = self._gso_convert_weight(package.shipping_weight) + request_body['Shipment'].update(self._gso_get_package_dimensions(package)) + request_body['Shipment']['ShipmentReference'] = package.name + request_body['Shipment']['TrackingNumber'] = self._gso_create_tracking_number(package.name) + try: + response = service.post_shipment(request_body) + + if response.get('ThermalLabel'): + labels['thermal'].append((response['TrackingNumber'], response['ThermalLabel'])) + elif response.get('PaperLabel'): + labels['paper'].append((response['TrackingNumber'], response['PaperLabel'])) + + if response.get('ShipmentCharges', {}).get('TotalCharge'): + cost += response['ShipmentCharges']['TotalCharge'] + except HTTPError as e: + raise ValidationError(e) + else: + request_body['Shipment']['Weight'] = self._gso_convert_weight(picking.shipping_weight) + request_body['Shipment'].update(self._gso_get_package_dimensions()) + request_body['Shipment']['ShipmentReference'] = picking.name + request_body['Shipment']['TrackingNumber'] = self._gso_create_tracking_number(picking.name) + try: + response = service.post_shipment(request_body) + + if response.get('ThermalLabel'): + labels['thermal'].append((response['TrackingNumber'], response['ThermalLabel'])) + elif response.get('PaperLabel'): + labels['paper'].append((response['TrackingNumber'], response['PaperLabel'])) + + if response.get('ShipmentCharges', {}).get('TotalCharge'): + cost += response['ShipmentCharges']['TotalCharge'] + except HTTPError as e: + raise ValidationError(e) + + # Handle results + trackings = [l[0] for l in labels['thermal']] + [l(0) for l in labels['paper']] + carrier_tracking_ref = ','.join(trackings) + + logmessage = _("Shipment created into GSO
" + "Tracking Numbers: %s") % (carrier_tracking_ref, ) + attachments = [] + if labels['thermal']: + attachments += [('LabelGSO-%s.zpl' % (l[0], ), l[1]) for l in labels['thermal']] + if labels['paper']: + attachments += [('LabelGSO-%s.pdf' % (l[0], ), l[1]) for l in labels['thermal']] + picking.message_post(body=logmessage, attachments=attachments) + shipping_data = {'exact_price': cost, + 'tracking_number': carrier_tracking_ref} + res.append(shipping_data) + return res + + def gso_cancel_shipment(self, picking): + sudoself = self.sudo() + service = sudoself._get_gso_service() + try: + request_body = { + 'AccountNumber': sudoself.gso_account_number, + } + for tracking in picking.carrier_tracking_ref.split(','): + request_body['TrackingNumber'] = tracking + _ = service.delete_shipment(request_body) + except HTTPError as e: + raise ValidationError(e) + picking.message_post(body=(_('Shipment N° %s has been cancelled') % (picking.carrier_tracking_ref, ))) + picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0}) + + def gso_rate_shipment(self, order): + sudoself = self.sudo() + service = sudoself._get_gso_service() + from_ = sudoself.get_shipper_warehouse(order=order) + to = sudoself.get_recipient(order=order) + address_type = 'B' if bool(to.company or to.parent_id.company) else 'R' + + est_weight_value = self._gso_convert_weight( + sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0) + + date_planned = None + if self.env.context.get('date_planned'): + date_planned = self.env.context.get('date_planned') + + ship_date_utc = fields.Datetime.from_string(date_planned if date_planned else fields.Datetime.now()) + ship_date_utc = ship_date_utc.replace(tzinfo=pytz.utc) + ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ)) + ship_date_gso = fields.Datetime.to_string(ship_date_gso) + + request_body = { + 'AccountNumber': sudoself.gso_account_number, + 'OriginZip': from_.zip, + 'DestinationZip': to.zip, + 'ShipDate': ship_date_gso, + 'PackageDimension': self._gso_get_package_dimensions(), + 'PackageWeight': est_weight_value, + 'DeliveryAddressType': address_type, + } + + result = service.get_rates_and_transit_time(request_body) + + delivery = list(filter(lambda d: d['ServiceCode'] == sudoself.gso_service_type, result['DeliveryServiceTypes'])) + if delivery: + delivery = delivery[0] + delivery_date_gso = delivery['DeliveryDate'].replace('T', ' ') + delivery_date_gso = fields.Datetime.from_string(delivery_date_gso) + delivery_date_gso = delivery_date_gso.replace(tzinfo=pytz.timezone(GSO_TZ)) + delivery_date_utc = delivery_date_gso.astimezone(pytz.utc) + delivery_date_utc = fields.Datetime.to_string(delivery_date_utc) + price = delivery.get('ShipmentCharges', {}).get('TotalCharge', 0.0) + return { + 'success': True, + 'price': price, + 'error_message': False, + 'date_delivered': delivery_date_utc, + 'warning_message': _('TotalCharge not found.') if price == 0.0 else False, + } + + raise Exception() + return { + 'success': False, + 'price': 0.0, + 'error_message': _('Delivery Method not found in result'), + 'warning_message': False, + } + + def gso_get_tracking_link(self, pickings): + # No way to get a link specifically as their site only allows POST into tracking form. + res = [] + for _ in pickings: + res.append('https://www.gso.com/Tracking') + return res diff --git a/delivery_gso/models/requests_gso.py b/delivery_gso/models/requests_gso.py new file mode 100644 index 00000000..e5fceb3a --- /dev/null +++ b/delivery_gso/models/requests_gso.py @@ -0,0 +1,47 @@ +import requests +from json import dumps + + +class GSORequest: + + BASE_URL = 'https://api.gso.com/Rest/v1' + + def __init__(self, production, username, password, account_number): + self.username = username + self.password = password + self.account_number = account_number + self.headers = self.make_headers() + self._get_token() + + def make_headers(self): + return { + 'Content-Type': 'application/json', + 'Accept-Encoding': 'gzip', + 'UserName': self.username, + 'PassWord': self.password, + 'AccountNumber': self.account_number, + } + + # Token Lasts 12 hours and should be refreshed accordingly. + # Might need to change to prevent too many calls to the API + def _get_token(self): + endpoint_url = self.BASE_URL + '/token' + response = requests.get(endpoint_url, headers=self.headers) + response.raise_for_status() + self.headers.update({'Token': response.headers['Token']}) + + def call(self, http_method, endpoint_url, payload): + url = self.BASE_URL + endpoint_url + result = requests.request(http_method, url, data=dumps(payload), headers=self.headers) + if result.status_code != 200: + raise requests.exceptions.HTTPError(result.text) + return result.json() + + def post_shipment(self, request_body): + return self.call('POST', '/Shipment', request_body) + + def delete_shipment(self, request_body): + return self.call('DELETE', '/Shipment', request_body) + + def get_rates_and_transit_time(self, request_body): + return self.call('POST', '/RatesAndTransitTimes', request_body) diff --git a/delivery_gso/views/delivery_gso_view.xml b/delivery_gso/views/delivery_gso_view.xml new file mode 100644 index 00000000..ec57d2fe --- /dev/null +++ b/delivery_gso/views/delivery_gso_view.xml @@ -0,0 +1,28 @@ + + + + + delivery.carrier.form.provider.gso + delivery.carrier + + + + + + + + + + + + + + + + + + + + + + From f1642115e13dce8d00d8899e2129cd30a5405e10 Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Thu, 5 Nov 2020 12:46:40 -0500 Subject: [PATCH 2/9] [MIG] delivery_gso: for Odoo 12.0 Fixed warning in manifest by extending underline "===..." --- delivery_gso/__manifest__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py index 3f94b3c9..2856bc7a 100644 --- a/delivery_gso/__manifest__.py +++ b/delivery_gso/__manifest__.py @@ -9,7 +9,8 @@ 'website': "https://hibou.io", 'description': """ Golden State Overnight (gso.com) Shipping -========================== +========================================= + * Provides estimates on shipping costs through gso.com. * Send your shippings through gso.com and allows tracking of packages. """, From c827d8b0e1cee45a4ab1993dd0d80b818f0579da Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Thu, 5 Nov 2020 13:05:30 -0500 Subject: [PATCH 3/9] [FIX] delivery_gso: removed outer parenthesis to fix message --- delivery_gso/models/delivery_gso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index 84bb6477..e553445e 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -254,7 +254,7 @@ class ProviderGSO(models.Model): _ = service.delete_shipment(request_body) except HTTPError as e: raise ValidationError(e) - picking.message_post(body=(_('Shipment N° %s has been cancelled') % (picking.carrier_tracking_ref, ))) + picking.message_post(body=_('Shipment N° %s has been cancelled') % (picking.carrier_tracking_ref, )) picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0}) def gso_rate_shipment(self, order): From edd1e5c9476290e0bb67b1272d29480ead9bb65c Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Thu, 12 Nov 2020 14:24:55 -0500 Subject: [PATCH 4/9] [MIG] delivery_gso: for Odoo 13.0 --- delivery_gso/models/delivery_gso.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index e553445e..de60b22e 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -160,7 +160,7 @@ class ProviderGSO(models.Model): company = self.get_shipper_company(picking=picking) from_ = self.get_shipper_warehouse(picking=picking) to = self.get_recipient(picking=picking) - address_type = 'B' if bool(to.company or to.parent_id.company) else 'R' + address_type = 'B' if "company" in (to.company_type, to.parent_id.company_type) else 'R' request_body = { 'AccountNumber': sudoself.gso_account_number, @@ -251,7 +251,7 @@ class ProviderGSO(models.Model): } for tracking in picking.carrier_tracking_ref.split(','): request_body['TrackingNumber'] = tracking - _ = service.delete_shipment(request_body) + __ = service.delete_shipment(request_body) except HTTPError as e: raise ValidationError(e) picking.message_post(body=_('Shipment N° %s has been cancelled') % (picking.carrier_tracking_ref, )) @@ -262,7 +262,7 @@ class ProviderGSO(models.Model): service = sudoself._get_gso_service() from_ = sudoself.get_shipper_warehouse(order=order) to = sudoself.get_recipient(order=order) - address_type = 'B' if bool(to.company or to.parent_id.company) else 'R' + address_type = 'B' if "company" in (to.company_type, to.parent_id.company_type) else 'R' est_weight_value = self._gso_convert_weight( sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0) From 46103eaaffd203cc8363722221a6ba571451b526 Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Wed, 18 Nov 2020 13:11:00 -0500 Subject: [PATCH 5/9] [MIG] delivery_gso: for Odoo 14.0 --- delivery_gso/__manifest__.py | 2 +- delivery_gso/models/delivery_gso.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py index 2856bc7a..f617443b 100644 --- a/delivery_gso/__manifest__.py +++ b/delivery_gso/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Golden State Overnight (gso.com) Shipping', 'summary': 'Send your shippings through gso.com and track them online.', - 'version': '11.0.1.0.0', + 'version': '14.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Warehouse', 'license': 'AGPL-3', diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index de60b22e..b68c397d 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -20,7 +20,7 @@ class ProductPackaging(models.Model): class ProviderGSO(models.Model): _inherit = 'delivery.carrier' - delivery_type = fields.Selection(selection_add=[('gso', 'gso.com')]) + delivery_type = fields.Selection(selection_add=[('gso', 'gso.com')], ondelete={'gso': 'cascade'}) gso_username = fields.Char(string='gso.com Username', groups='base.group_system') gso_password = fields.Char(string='gso.com Password', groups='base.group_system') gso_account_number = fields.Char(string='gso.com Account Number', groups='base.group_system') @@ -139,7 +139,7 @@ class ProviderGSO(models.Model): package_type = self.gso_default_packaging_id else: package_type = package.packaging_id - return {'Length': package_type.length, 'Width': package_type.width, 'Height': package_type.height} + return {'Length': package_type.packaging_length, 'Width': package_type.width, 'Height': package_type.height} def _gso_convert_weight(self, weight_in_kg): # m(lb) = m(kg) / 0.45359237 From 9ea00c570f727e390c4abb46f248072c7f8b097b Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Tue, 14 Sep 2021 11:42:47 -0500 Subject: [PATCH 6/9] [IMP] delivery_gso: move gso_rate_shipment_multi into 14.0 --- delivery_gso/models/delivery_gso.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index b68c397d..597a5b59 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -7,6 +7,8 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError, UserError from .requests_gso import GSORequest +import logging +_logger = logging.getLogger(__name__) GSO_TZ = 'PST8PDT' @@ -319,3 +321,88 @@ class ProviderGSO(models.Model): for _ in pickings: res.append('https://www.gso.com/Tracking') return res + + def gso_rate_shipment_multi(self, order=None, picking=None): + sudoself = self.sudo() + service = sudoself._get_gso_service() + from_ = sudoself.get_shipper_warehouse(order=order, picking=picking) + to = sudoself.get_recipient(order=order, picking=picking) + address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R' + + if order: + est_weight_value = self._gso_convert_weight( + sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0) + else: + est_weight_value = self._gso_convert_weight(picking.shipping_weight) + + date_planned = fields.Datetime.now() + if self.env.context.get('date_planned'): + date_planned = self.env.context.get('date_planned') + + ship_date_utc = fields.Datetime.from_string(date_planned if date_planned else fields.Datetime.now()) + ship_date_utc = ship_date_utc.replace(tzinfo=pytz.utc) + ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ)) + ship_date_gso = fields.Datetime.to_string(ship_date_gso) + + if picking and picking.package_ids: + package_dimensions = self._gso_get_package_dimensions(package=picking.package_ids[0]) + else: + package_dimensions = self._gso_get_package_dimensions() + + request_body = { + 'AccountNumber': sudoself.gso_account_number, + 'OriginZip': from_.zip, + 'DestinationZip': to.zip, + 'ShipDate': ship_date_gso, + 'PackageDimension': package_dimensions, + 'PackageWeight': est_weight_value, + 'DeliveryAddressType': address_type, + } + + try: + result = service.get_rates_and_transit_time(request_body) + # _logger.warn('GSO result:\n%s' % result) + except HTTPError as e: + _logger.error(e) + return [{ + 'success': False, + 'price': 0.0, + 'error_message': _('GSO web service returned an error.'), + 'warning_message': False, + }] + + # delivery = list(filter(lambda d: d['ServiceCode'] == sudoself.gso_service_type, result['DeliveryServiceTypes'])) + # if delivery: + rates = [] + for delivery in result['DeliveryServiceTypes']: + delivery_date_gso = delivery['DeliveryDate'].replace('T', ' ') + delivery_date_gso = fields.Datetime.from_string(delivery_date_gso) + delivery_date_gso = delivery_date_gso.replace(tzinfo=pytz.timezone(GSO_TZ)) + delivery_date_utc = delivery_date_gso.astimezone(pytz.utc) + delivery_date_utc = fields.Datetime.to_string(delivery_date_utc) + price = delivery.get('ShipmentCharges', {}).get('TotalCharge', 0.0) + + carrier = self.gso_find_delivery_carrier_for_service(delivery['ServiceCode']) + if carrier: + rates.append({ + 'carrier': carrier, + 'success': True, + 'price': price, + 'error_message': False, + 'warning_message': _('TotalCharge not found.') if price == 0.0 else False, + 'date_planned': date_planned, + 'date_delivered': delivery_date_utc, + 'transit_days': False, + 'service_code': delivery['ServiceCode'], + }) + + return rates + + def gso_find_delivery_carrier_for_service(self, service_code): + if self.gso_service_type == service_code: + return self + # arbitrary decision, lets find the same account number + carrier = self.search([('gso_account_number', '=', self.gso_account_number), + ('gso_service_type', '=', service_code) + ], limit=1) + return carrier From 23d84e77994823863a38e687b8942e920a46c0fe Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 20 Sep 2021 08:18:20 -0700 Subject: [PATCH 7/9] [FIX] delivery_gso: paper label handling --- delivery_gso/models/delivery_gso.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index 597a5b59..513d398b 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -1,5 +1,6 @@ import pytz from math import ceil +from base64 import b64decode from requests import HTTPError from hashlib import sha1 @@ -13,6 +14,13 @@ _logger = logging.getLogger(__name__) GSO_TZ = 'PST8PDT' +def inline_b64decode(data): + try: + return b64decode(data) + except: + return '' + + class ProductPackaging(models.Model): _inherit = 'product.packaging' @@ -228,7 +236,7 @@ class ProviderGSO(models.Model): raise ValidationError(e) # Handle results - trackings = [l[0] for l in labels['thermal']] + [l(0) for l in labels['paper']] + trackings = [l[0] for l in labels['thermal']] + [l[0] for l in labels['paper']] carrier_tracking_ref = ','.join(trackings) logmessage = _("Shipment created into GSO
" @@ -237,7 +245,8 @@ class ProviderGSO(models.Model): if labels['thermal']: attachments += [('LabelGSO-%s.zpl' % (l[0], ), l[1]) for l in labels['thermal']] if labels['paper']: - attachments += [('LabelGSO-%s.pdf' % (l[0], ), l[1]) for l in labels['thermal']] + # paper labels re-encoded base64 + attachments += [('LabelGSO-%s.png' % (l[0], ), inline_b64decode(l[1])) for l in labels['paper']] picking.message_post(body=logmessage, attachments=attachments) shipping_data = {'exact_price': cost, 'tracking_number': carrier_tracking_ref} From 29b69b35b1536705bbcc0c2bcd148e5601e95e6b Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 20 Sep 2021 17:47:22 -0700 Subject: [PATCH 8/9] [IMP] delivery_gso: mechansims for per-package rating and shipping --- delivery_gso/__init__.py | 2 + delivery_gso/__manifest__.py | 4 +- delivery_gso/models/__init__.py | 2 + delivery_gso/models/delivery_gso.py | 57 ++++++++++++++++++++++------- delivery_gso/models/requests_gso.py | 2 + 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/delivery_gso/__init__.py b/delivery_gso/__init__.py index 0650744f..09434554 100644 --- a/delivery_gso/__init__.py +++ b/delivery_gso/__init__.py @@ -1 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from . import models diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py index f617443b..8dcc7785 100644 --- a/delivery_gso/__manifest__.py +++ b/delivery_gso/__manifest__.py @@ -1,10 +1,10 @@ { 'name': 'Golden State Overnight (gso.com) Shipping', 'summary': 'Send your shippings through gso.com and track them online.', - 'version': '14.0.1.0.0', + 'version': '14.0.1.1.0', 'author': "Hibou Corp.", 'category': 'Warehouse', - 'license': 'AGPL-3', + 'license': 'OPL-1', 'images': [], 'website': "https://hibou.io", 'description': """ diff --git a/delivery_gso/models/__init__.py b/delivery_gso/models/__init__.py index 943392d3..c9a65a9e 100644 --- a/delivery_gso/models/__init__.py +++ b/delivery_gso/models/__init__.py @@ -1 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from . import delivery_gso diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index 513d398b..3df02556 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + import pytz from math import ceil from base64 import b64decode @@ -198,9 +200,15 @@ class ProviderGSO(models.Model): 'thermal': [], 'paper': [], } - if picking.package_ids: + picking_packages = picking.package_ids + package_carriers = picking_packages.mapped('carrier_id') + if package_carriers: + # only ship ours + picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref) + + if picking_packages: # Every package will be a transaction - for package in picking.package_ids: + for package in picking_packages: request_body['Shipment']['Weight'] = self._gso_convert_weight(package.shipping_weight) request_body['Shipment'].update(self._gso_get_package_dimensions(package)) request_body['Shipment']['ShipmentReference'] = package.name @@ -217,7 +225,8 @@ class ProviderGSO(models.Model): cost += response['ShipmentCharges']['TotalCharge'] except HTTPError as e: raise ValidationError(e) - else: + elif not package_carriers: + # ship the whole picking request_body['Shipment']['Weight'] = self._gso_convert_weight(picking.shipping_weight) request_body['Shipment'].update(self._gso_get_package_dimensions()) request_body['Shipment']['ShipmentReference'] = picking.name @@ -234,6 +243,8 @@ class ProviderGSO(models.Model): cost += response['ShipmentCharges']['TotalCharge'] except HTTPError as e: raise ValidationError(e) + else: + continue # Handle results trackings = [l[0] for l in labels['thermal']] + [l[0] for l in labels['paper']] @@ -331,18 +342,32 @@ class ProviderGSO(models.Model): res.append('https://www.gso.com/Tracking') return res - def gso_rate_shipment_multi(self, order=None, picking=None): + def gso_rate_shipment_multi(self, order=None, picking=None, packages=None): + if not packages: + return self._gso_rate_shipment_multi_package(order=order, picking=picking) + else: + rates = [] + for package in packages: + rates += self._gso_rate_shipment_multi_package(order=order, picking=picking, package=package) + return rates + + def _gso_rate_shipment_multi_package(self, order=None, picking=None, package=None): sudoself = self.sudo() - service = sudoself._get_gso_service() + try: + service = sudoself._get_gso_service() + except HTTPError as e: + _logger.error(e) + return [{ + 'success': False, + 'price': 0.0, + 'error_message': _('GSO web service returned an error. ' + str(e)), + 'warning_message': False, + }] + from_ = sudoself.get_shipper_warehouse(order=order, picking=picking) to = sudoself.get_recipient(order=order, picking=picking) address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R' - - if order: - est_weight_value = self._gso_convert_weight( - sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0) - else: - est_weight_value = self._gso_convert_weight(picking.shipping_weight) + package_dimensions = self._gso_get_package_dimensions(package=package) date_planned = fields.Datetime.now() if self.env.context.get('date_planned'): @@ -353,10 +378,13 @@ class ProviderGSO(models.Model): ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ)) ship_date_gso = fields.Datetime.to_string(ship_date_gso) - if picking and picking.package_ids: - package_dimensions = self._gso_get_package_dimensions(package=picking.package_ids[0]) + if order: + est_weight_value = self._gso_convert_weight( + sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0) + elif not package: + est_weight_value = self._gso_convert_weight(picking.shipping_weight) else: - package_dimensions = self._gso_get_package_dimensions() + est_weight_value = package.shipping_weight or package.weight request_body = { 'AccountNumber': sudoself.gso_account_number, @@ -395,6 +423,7 @@ class ProviderGSO(models.Model): if carrier: rates.append({ 'carrier': carrier, + 'package': package or self.env['stock.quant.package'].browse(), 'success': True, 'price': price, 'error_message': False, diff --git a/delivery_gso/models/requests_gso.py b/delivery_gso/models/requests_gso.py index e5fceb3a..4e512661 100644 --- a/delivery_gso/models/requests_gso.py +++ b/delivery_gso/models/requests_gso.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + import requests from json import dumps From 1cd9fe3c463665f9c7411a5304cb1bbf458b0dac Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 6 Oct 2021 08:27:04 -0700 Subject: [PATCH 9/9] [MIG] delivery_gso: for Odoo 15.0 --- delivery_gso/__manifest__.py | 2 +- delivery_gso/models/delivery_gso.py | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py index 8dcc7785..e1923d56 100644 --- a/delivery_gso/__manifest__.py +++ b/delivery_gso/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Golden State Overnight (gso.com) Shipping', 'summary': 'Send your shippings through gso.com and track them online.', - 'version': '14.0.1.1.0', + 'version': '15.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Warehouse', 'license': 'OPL-1', diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index 3df02556..04f55595 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -23,10 +23,10 @@ def inline_b64decode(data): return '' -class ProductPackaging(models.Model): - _inherit = 'product.packaging' +class StockPackageType(models.Model): + _inherit = 'stock.package.type' - package_carrier_type = fields.Selection(selection_add=[('gso', 'gso.com')]) + package_carrier_type = fields.Selection(selection_add=[('gso', 'gso.com')], ondelete={'gso': 'set default'}) class ProviderGSO(models.Model): @@ -36,7 +36,7 @@ class ProviderGSO(models.Model): gso_username = fields.Char(string='gso.com Username', groups='base.group_system') gso_password = fields.Char(string='gso.com Password', groups='base.group_system') gso_account_number = fields.Char(string='gso.com Account Number', groups='base.group_system') - gso_default_packaging_id = fields.Many2one('product.packaging', string='Default Package Type') + gso_default_packaging_id = fields.Many2one('stock.package.type', string='Default Package Type') # For service type, SAM, SPM, and SEV require authorized accounts. gso_service_type = fields.Selection([('PDS', 'Priority Overnight'), ('EPS', 'Early Priority Overnight'), @@ -151,11 +151,20 @@ class ProviderGSO(models.Model): package_type = self.gso_default_packaging_id else: package_type = package.packaging_id + length_uom = self.env['product.template']._get_length_uom_id_from_ir_config_parameter() + if length_uom.name == 'ft': + return {'Length': round(package_type.packaging_length / 12.0), 'Width': round(package_type.width / 12.0), 'Height': round(package_type.height / 12.0)} + elif length_uom.name == 'mm': + return {'Length': round(package_type.packaging_length * 0.0393701), 'Width': round(package_type.width * 0.0393701), 'Height': round(package_type.height * 0.0393701)} return {'Length': package_type.packaging_length, 'Width': package_type.width, 'Height': package_type.height} - def _gso_convert_weight(self, weight_in_kg): - # m(lb) = m(kg) / 0.45359237 - weight_in_lb = weight_in_kg / 0.45359237 + def _gso_convert_weight(self, weight_in_db): + weight_uom = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() + if weight_uom.name == 'kg': + weight_in_lb = weight_in_db / 0.45359237 + else: + # assume lbs + weight_in_lb = weight_in_db # If less than 8 oz... if weight_in_lb < 0.5: return 0