Files
suite/delivery_gso/models/delivery_gso.py
2022-01-11 12:54:14 -08:00

456 lines
20 KiB
Python

# 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
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
import logging
_logger = logging.getLogger(__name__)
GSO_TZ = 'PST8PDT'
def inline_b64decode(data):
try:
return b64decode(data)
except:
return ''
class StockPackageType(models.Model):
_inherit = 'stock.package.type'
package_carrier_type = fields.Selection(selection_add=[('gso', 'gso.com')], ondelete={'gso': 'set default'})
class ProviderGSO(models.Model):
_inherit = 'delivery.carrier'
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')
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'),
('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.package_type_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_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
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 "company" in (to.company_type, to.parent_id.company_type) 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': [],
}
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_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
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)
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
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)
else:
continue
# 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<br/>"
"<b>Tracking Numbers:</b> %s") % (carrier_tracking_ref, )
attachments = []
if labels['thermal']:
attachments += [('LabelGSO-%s.zpl' % (l[0], ), l[1]) for l in labels['thermal']]
if labels['paper']:
# 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}
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 "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)
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
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()
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'
package_dimensions = self._gso_get_package_dimensions(package=package)
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 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:
est_weight_value = package.shipping_weight or package.weight
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.warning('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,
'package': package or self.env['stock.quant.package'].browse(),
'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