mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[REL] connector_amazon_sp: for 11.0
This commit is contained in:
415
connector_amazon_sp/models/delivery_carrier/common.py
Normal file
415
connector_amazon_sp/models/delivery_carrier/common.py
Normal file
@@ -0,0 +1,415 @@
|
||||
# © 2021 Hibou Corp.
|
||||
|
||||
import zlib
|
||||
from datetime import date, datetime
|
||||
from base64 import b64decode
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
|
||||
amazon_sp_mfn_allowed_services = fields.Text(
|
||||
string='Amazon SP MFN Allowed Methods',
|
||||
help='Comma separated list. e.g. "FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB"')
|
||||
|
||||
|
||||
class ProviderAmazonSP(models.Model):
|
||||
_inherit = 'delivery.carrier'
|
||||
|
||||
delivery_type = fields.Selection(selection_add=[
|
||||
# ('amazon_sp', 'Amazon Selling Partner'), # TODO buy shipping for regular orders?
|
||||
('amazon_sp_mfn', 'Amazon SP Merchant Fulfillment')
|
||||
])
|
||||
|
||||
# Fields when uploading shipping to Amazon
|
||||
amazon_sp_carrier_code = fields.Char(string='Amazon Carrier Code',
|
||||
help='Specific carrier code, will default to "Other".')
|
||||
amazon_sp_carrier_name = fields.Char(string='Amazon Carrier Name',
|
||||
help='Specific carrier name, will default to regular name.')
|
||||
amazon_sp_shipping_method = fields.Char(string='Amazon Shipping Method',
|
||||
help='Specific shipping method, will default to "Standard"')
|
||||
# Fields when purchasing shipping from Amazon
|
||||
amazon_sp_mfn_allowed_services = fields.Text(
|
||||
string='Allowed Methods',
|
||||
help='Comma separated list. e.g. "FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB"',
|
||||
default='FEDEX_PTP_HOME_DELIVERY,FEDEX_PTP_SECOND_DAY,USPS_PTP_PRI_LFRB')
|
||||
amazon_sp_mfn_label_formats = fields.Text(
|
||||
string='Allowed Label Formats',
|
||||
help='Comma separated list. e.g. "ZPL203,PNG"',
|
||||
default='ZPL203,PNG')
|
||||
|
||||
def send_shipping(self, pickings):
|
||||
pickings = pickings.with_context(amz_pii_decrypt=1)
|
||||
self = self.with_context(amz_pii_decrypt=1)
|
||||
return super(ProviderAmazonSP, self).send_shipping(pickings)
|
||||
|
||||
def is_amazon(self, order=None, picking=None):
|
||||
# Override from `delivery_hibou` to be used in stamps etc....
|
||||
if picking and picking.sale_id:
|
||||
so = picking.sale_id
|
||||
if so.amazon_bind_ids:
|
||||
return True
|
||||
if order and order.amazon_bind_ids:
|
||||
return True
|
||||
return super().is_amazon(order=order, picking=picking)
|
||||
|
||||
def _amazon_sp_mfn_get_order_details(self, order):
|
||||
company = self.get_shipper_company(order=order)
|
||||
wh_partner = self.get_shipper_warehouse(order=order)
|
||||
|
||||
if not order.amazon_bind_ids:
|
||||
raise ValidationError('Amazon shipping is not available for this order.')
|
||||
|
||||
amazon_order_id = order.amazon_bind_ids[0].external_id
|
||||
from_ = dict(
|
||||
Name=company.name,
|
||||
AddressLine1=wh_partner.street,
|
||||
AddressLine2=wh_partner.street2 or '',
|
||||
City=wh_partner.city,
|
||||
StateOrProvinceCode=wh_partner.state_id.code,
|
||||
PostalCode=wh_partner.zip,
|
||||
CountryCode=wh_partner.country_id.code,
|
||||
Email=company.email or '',
|
||||
Phone=company.phone or '',
|
||||
)
|
||||
return amazon_order_id, from_
|
||||
|
||||
def _amazon_sp_mfn_get_items_for_order(self, order):
|
||||
items = order.order_line.filtered(lambda l: l.amazon_bind_ids)
|
||||
return items.mapped(lambda l: (l.amazon_bind_ids[0].external_id, str(int(l.product_qty))))
|
||||
|
||||
def _amazon_sp_mfn_get_items_for_package(self, package, order):
|
||||
items = []
|
||||
if not package.quant_ids:
|
||||
for move_line in package.current_picking_move_line_ids:
|
||||
line = order.order_line.filtered(lambda l: l.product_id.id == move_line.product_id.id and l.amazon_bind_ids)
|
||||
if line:
|
||||
items.append((line[0].amazon_bind_ids[0].external_id, int(move_line.qty_done), {
|
||||
'Unit': 'g',
|
||||
'Value': line.product_id.weight * move_line.qty_done * 1000,
|
||||
}, line.name))
|
||||
else:
|
||||
for quant in package.quant_ids:
|
||||
line = order.order_line.filtered(lambda l: l.product_id.id == quant.product_id.id and l.amazon_bind_ids)
|
||||
if line:
|
||||
items.append((line[0].amazon_bind_ids[0].external_id, int(quant.quantity), {
|
||||
'Unit': 'g',
|
||||
'Value': line.product_id.weight * quant.quantity * 1000,
|
||||
}, line.name))
|
||||
return items
|
||||
|
||||
def _amazon_sp_mfn_convert_weight(self, weight):
|
||||
return int(weight * 1000), 'g'
|
||||
|
||||
def _amazon_sp_mfn_pick_service(self, api_services, package=None):
|
||||
allowed_services = self.amazon_sp_mfn_allowed_services.split(',')
|
||||
if package and package.packaging_id.amazon_sp_mfn_allowed_services:
|
||||
allowed_services = package.packaging_id.amazon_sp_mfn_allowed_services.split(',')
|
||||
|
||||
allowed_label_formats = self.amazon_sp_mfn_label_formats.split(',')
|
||||
services = []
|
||||
api_service_list = api_services['ShippingServiceList']
|
||||
if not isinstance(api_service_list, list):
|
||||
api_service_list = [api_service_list]
|
||||
for s in api_service_list:
|
||||
if s['ShippingServiceId'] in allowed_services:
|
||||
s_available_formats = s['AvailableLabelFormats']
|
||||
for l in allowed_label_formats:
|
||||
if l in s_available_formats:
|
||||
services.append({
|
||||
'service_id': s['ShippingServiceId'],
|
||||
'amount': float(s['Rate']['Amount']),
|
||||
'label_format': l
|
||||
})
|
||||
break
|
||||
if services:
|
||||
return sorted(services, key=lambda s: s['amount'])[0]
|
||||
|
||||
error = 'Cannot find applicable service. API Services: ' + \
|
||||
','.join([s['ShippingServiceId'] for s in api_services['ShippingServiceList']]) + \
|
||||
' Allowed Services: ' + self.amazon_sp_mfn_allowed_services
|
||||
raise ValidationError(error)
|
||||
|
||||
def amazon_sp_mfn_send_shipping(self, pickings):
|
||||
res = []
|
||||
date_planned = datetime.now().replace(microsecond=0).isoformat()
|
||||
|
||||
for picking in pickings:
|
||||
shipments = []
|
||||
|
||||
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 not picking_packages:
|
||||
continue
|
||||
|
||||
order = picking.sale_id.sudo() # for having access to amazon bindings and backend
|
||||
|
||||
# API comes from the binding backend
|
||||
if order.amazon_bind_ids:
|
||||
amazon_order = order.amazon_bind_ids[0]
|
||||
api_wrapped = amazon_order.backend_id.get_wrapped_api()
|
||||
# must_arrive_by_date not used, and `amazon_order.requested_date` can be False
|
||||
# so if it is to be used, we must decide what to do if there is no date.
|
||||
# must_arrive_by_date = fields.Datetime.from_string(amazon_order.requested_date).isoformat()
|
||||
api = api_wrapped.merchant_fulfillment()
|
||||
|
||||
if not api:
|
||||
raise UserError('%s cannot be shipped by Amazon SP without a Sale Order or backend to use.' % picking)
|
||||
|
||||
amazon_order_id, from_ = self._amazon_sp_mfn_get_order_details(order)
|
||||
for package in picking_packages:
|
||||
dimensions = {
|
||||
'Length': package.packaging_id.length or 0.1,
|
||||
'Width': package.packaging_id.width or 0.1,
|
||||
'Height': package.packaging_id.height or 0.1,
|
||||
'Unit': 'inches',
|
||||
}
|
||||
weight, weight_unit = self._amazon_sp_mfn_convert_weight(package.shipping_weight)
|
||||
items = self._amazon_sp_mfn_get_items_for_package(package, order)
|
||||
# Declared value
|
||||
inventory_value = self.get_inventory_value(picking=picking, package=package)
|
||||
sig_req = self.get_signature_required(picking=picking, package=package)
|
||||
|
||||
ShipmentRequestDetails = {
|
||||
'AmazonOrderId': amazon_order_id,
|
||||
'ShipFromAddress': from_,
|
||||
'Weight': {'Unit': weight_unit, 'Value': weight},
|
||||
'SellerOrderId': order.name,
|
||||
# The format of these dates cannot be determined, attempts:
|
||||
# 2021-04-27 08:00:00
|
||||
# 2021-04-27T08:00:00
|
||||
# 2021-04-27T08:00:00Z
|
||||
# 2021-04-27T08:00:00+00:00
|
||||
# 'ShipDate': date_planned,
|
||||
# 'MustArriveByDate': must_arrive_by_date,
|
||||
'ShippingServiceOptions': {
|
||||
'DeliveryExperience': 'DeliveryConfirmationWithSignature' if sig_req else 'NoTracking',
|
||||
# CarrierWillPickUp is required
|
||||
'CarrierWillPickUp': False, # Note: Scheduled carrier pickup is available only using Dynamex (US), DPD (UK), and Royal Mail (UK).
|
||||
'DeclaredValue': {
|
||||
'Amount': inventory_value,
|
||||
'CurrencyCode': 'USD'
|
||||
},
|
||||
# Conflicts at time of shipping for the above
|
||||
# 'CarrierWillPickUpOption': 'NoPreference',
|
||||
'LabelFormat': 'ZPL203'
|
||||
},
|
||||
'ItemList': [{
|
||||
'OrderItemId': i[0],
|
||||
'Quantity': i[1],
|
||||
'ItemWeight': i[2],
|
||||
'ItemDescription': i[3],
|
||||
} for i in items],
|
||||
'PackageDimensions': dimensions,
|
||||
}
|
||||
|
||||
try:
|
||||
# api_services = api.get_eligible_shipment_services(ShipmentRequestDetails, ShippingOfferingFilter={
|
||||
# 'IncludePackingSlipWithLabel': False,
|
||||
# 'IncludeComplexShippingOptions': False,
|
||||
# 'CarrierWillPickUp': 'CarrierWillPickUp',
|
||||
# 'DeliveryExperience': 'NoTracking',
|
||||
# })
|
||||
api_services = api.get_eligible_shipment_services(ShipmentRequestDetails)
|
||||
except api_wrapped.SellingApiForbiddenException:
|
||||
raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
|
||||
except api_wrapped.SellingApiException as e:
|
||||
raise UserError('API Exception: ' + str(e.message))
|
||||
|
||||
api_services = api_services.payload
|
||||
service = self._amazon_sp_mfn_pick_service(api_services, package=package)
|
||||
|
||||
try:
|
||||
shipment = api.create_shipment(ShipmentRequestDetails, service['service_id']).payload
|
||||
except api_wrapped.SellingApiForbiddenException:
|
||||
raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
|
||||
except api_wrapped.SellingApiException as e:
|
||||
raise UserError('API Exception: ' + str(e.message))
|
||||
|
||||
shipments.append((shipment, service))
|
||||
|
||||
carrier_price = 0.0
|
||||
tracking_numbers =[]
|
||||
for shipment, service in shipments:
|
||||
tracking_number = shipment['TrackingId']
|
||||
carrier_name = shipment['ShippingService']['CarrierName']
|
||||
label_data = shipment['Label']['FileContents']['Contents']
|
||||
|
||||
# So far, this is b64encoded and gzipped
|
||||
try:
|
||||
label_decoded = b64decode(label_data)
|
||||
try:
|
||||
label_decoded = zlib.decompress(label_decoded)
|
||||
except:
|
||||
label_decoded = zlib.decompress(label_decoded, zlib.MAX_WBITS | 16)
|
||||
label_data = label_decoded
|
||||
except:
|
||||
# Oh well...
|
||||
pass
|
||||
|
||||
body = 'Shipment created into Amazon MFN<br/> <b>Tracking Number : <br/>' + tracking_number + '</b>'
|
||||
picking.message_post(body=body, attachments=[('Label%s-%s.%s' % (carrier_name, tracking_number, service['label_format']), label_data)])
|
||||
carrier_price += float(shipment['ShippingService']['Rate']['Amount'])
|
||||
tracking_numbers.append(tracking_number)
|
||||
shipping_data = {'exact_price': carrier_price, 'tracking_number': ','.join(tracking_numbers)}
|
||||
res = res + [shipping_data]
|
||||
|
||||
return res
|
||||
|
||||
def amazon_sp_mfn_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||
if not packages:
|
||||
return self._amazon_sp_mfn_rate_shipment_multi_package(order=order, picking=picking)
|
||||
else:
|
||||
rates = []
|
||||
for package in packages:
|
||||
rates += self._amazon_sp_mfn_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||
return rates
|
||||
|
||||
def _amazon_sp_mfn_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||
res = []
|
||||
self.ensure_one()
|
||||
date_planned = fields.Datetime.now()
|
||||
if self.env.context.get('date_planned'):
|
||||
date_planned = self.env.context.get('date_planned')
|
||||
|
||||
if order or not picking:
|
||||
raise UserError('Amazon SP MFN is intended to be used on imported orders.')
|
||||
if package:
|
||||
packages = package
|
||||
else:
|
||||
packages = picking.package_ids
|
||||
|
||||
if not packages:
|
||||
raise UserError('Amazon SP MFN can only be used with packed items.')
|
||||
|
||||
# to use current inventory in package
|
||||
packages = packages.with_context(picking_id=picking.id)
|
||||
|
||||
order = picking.sale_id.sudo()
|
||||
api = None
|
||||
if order.amazon_bind_ids:
|
||||
amazon_order = order.amazon_bind_ids[0]
|
||||
api_wrapped = amazon_order.backend_id.get_wrapped_api()
|
||||
# must_arrive_by_date not used, and `amazon_order.requested_date` can be False
|
||||
# so if it is to be used, we must decide what to do if there is no date.
|
||||
# must_arrive_by_date = fields.Datetime.from_string(amazon_order.requested_date).isoformat()
|
||||
api = api_wrapped.merchant_fulfillment()
|
||||
|
||||
if not api:
|
||||
raise UserError('%s cannot be shipped by Amazon SP without a Sale Order or backend to use.' % picking)
|
||||
|
||||
amazon_order_id, from_ = self._amazon_sp_mfn_get_order_details(order)
|
||||
for package in packages:
|
||||
dimensions = {
|
||||
'Length': package.packaging_id.length or 0.1,
|
||||
'Width': package.packaging_id.width or 0.1,
|
||||
'Height': package.packaging_id.height or 0.1,
|
||||
'Unit': 'inches',
|
||||
}
|
||||
weight, weight_unit = self._amazon_sp_mfn_convert_weight(package.shipping_weight)
|
||||
items = self._amazon_sp_mfn_get_items_for_package(package, order)
|
||||
# Declared value
|
||||
inventory_value = self.get_insurance_value(picking=picking, package=package)
|
||||
sig_req = self.get_signature_required(picking=picking, package=packages)
|
||||
|
||||
|
||||
ShipmentRequestDetails = {
|
||||
'AmazonOrderId': amazon_order_id,
|
||||
'ShipFromAddress': from_,
|
||||
'Weight': {'Unit': weight_unit, 'Value': weight},
|
||||
'SellerOrderId': order.name,
|
||||
# The format of these dates cannot be determined, attempts:
|
||||
# 2021-04-27 08:00:00
|
||||
# 2021-04-27T08:00:00
|
||||
# 2021-04-27T08:00:00Z
|
||||
# 2021-04-27T08:00:00+00:00
|
||||
# 'ShipDate': date_planned,
|
||||
# 'MustArriveByDate': must_arrive_by_date,
|
||||
'ShippingServiceOptions': {
|
||||
'DeliveryExperience': 'DeliveryConfirmationWithSignature' if sig_req else 'NoTracking',
|
||||
# CarrierWillPickUp is required
|
||||
'CarrierWillPickUp': False,
|
||||
# Note: Scheduled carrier pickup is available only using Dynamex (US), DPD (UK), and Royal Mail (UK).
|
||||
'DeclaredValue': {
|
||||
'Amount': inventory_value,
|
||||
'CurrencyCode': 'USD'
|
||||
},
|
||||
# Conflicts at time of shipping for the above
|
||||
# 'CarrierWillPickUpOption': 'NoPreference',
|
||||
'LabelFormat': 'ZPL203'
|
||||
},
|
||||
'ItemList': [{
|
||||
'OrderItemId': i[0],
|
||||
'Quantity': i[1],
|
||||
'ItemWeight': i[2],
|
||||
'ItemDescription': i[3],
|
||||
} for i in items],
|
||||
'PackageDimensions': dimensions,
|
||||
}
|
||||
|
||||
try:
|
||||
# api_services = api.get_eligible_shipment_services(ShipmentRequestDetails, ShippingOfferingFilter={
|
||||
# 'IncludePackingSlipWithLabel': False,
|
||||
# 'IncludeComplexShippingOptions': False,
|
||||
# 'CarrierWillPickUp': 'CarrierWillPickUp',
|
||||
# 'DeliveryExperience': 'NoTracking',
|
||||
# })
|
||||
api_services = api.get_eligible_shipment_services(ShipmentRequestDetails)
|
||||
except api_wrapped.SellingApiForbiddenException:
|
||||
raise UserError('Your Amazon SP API access does not include MerchantFulfillment')
|
||||
except api_wrapped.SellingApiException as e:
|
||||
raise UserError('API Exception: ' + str(e.message))
|
||||
|
||||
api_services = api_services.payload
|
||||
# project into distinct carrier
|
||||
allowed_services = self.amazon_sp_mfn_allowed_services.split(',')
|
||||
if package and package.packaging_id.amazon_sp_mfn_allowed_services:
|
||||
allowed_services = package.packaging_id.amazon_sp_mfn_allowed_services.split(',')
|
||||
|
||||
api_service_list = api_services['ShippingServiceList']
|
||||
if not isinstance(api_service_list, list):
|
||||
api_service_list = [api_service_list]
|
||||
|
||||
for s in filter(lambda s: s['ShippingServiceId'] in allowed_services, api_service_list):
|
||||
_logger.warning('ShippingService: ' + str(s))
|
||||
service_code = s['ShippingServiceId']
|
||||
carrier = self.amazon_sp_mfn_find_delivery_carrier_for_service(service_code)
|
||||
if carrier:
|
||||
res.append({
|
||||
'carrier': carrier,
|
||||
'package': package or self.env['stock.quant.package'].browse(),
|
||||
'success': True,
|
||||
'price': s['Rate']['Amount'],
|
||||
'error_message': False,
|
||||
'warning_message': False,
|
||||
# 'transit_days': transit_days,
|
||||
'date_delivered': s['LatestEstimatedDeliveryDate'] if s['LatestEstimatedDeliveryDate'] else s['EarliestEstimatedDeliveryDate'],
|
||||
'date_planned': date_planned,
|
||||
'service_code': service_code,
|
||||
})
|
||||
if not res:
|
||||
res.append({
|
||||
'success': False,
|
||||
'price': 0.0,
|
||||
'error_message': 'No valid rates returned from AmazonSP-MFN',
|
||||
'warning_message': False
|
||||
})
|
||||
return res
|
||||
|
||||
def amazon_sp_mfn_find_delivery_carrier_for_service(self, service_code):
|
||||
if self.amazon_sp_mfn_allowed_services == service_code:
|
||||
return self
|
||||
carrier = self.search([('amazon_sp_mfn_allowed_services', '=', service_code),
|
||||
('delivery_type', '=', 'amazon_sp_mfn')
|
||||
], limit=1)
|
||||
return carrier
|
||||
Reference in New Issue
Block a user