1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/delivery_sendcloud/models/sendcloud_service.py
2024-12-10 09:04:09 +07:00

635 lines
34 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import math
import requests
from werkzeug.urls import url_join
from odoo import fields, _
from odoo.exceptions import UserError
from odoo.tools import float_repr, float_compare, float_is_zero
# More information at : https://api.sendcloud.dev/docs/sendcloud-public-api/integrations
BASE_URL = "https://panel.sendcloud.sc/api/v2/"
MULTICOLLO_MAX_PACKAGE = 20
class SendCloud:
def __init__(self, public_key, private_key, logger):
self.logger = logger
self.session = requests.Session()
self.session.auth = (public_key, private_key)
def _get_shipping_functionalities(self):
return self._send_request('shipping-functionalities')
def _get_shipping_products(self, from_country, is_return=False, carrier=None, weight=None, to_country=None, sizes=None, size_unit='centimeter'):
params = {'from_country': from_country, 'returns': is_return}
if carrier:
params.update({'carrier': carrier})
if to_country:
params.update({'to_country': to_country})
if weight:
params.update({'weight': weight, 'weight_unit': 'gram'})
if sizes:
params.update({
'length': sizes.get('length', 0),
'height': sizes.get('height', 0),
'width': sizes.get('width', 0),
'length_unit': size_unit,
'height_unit': size_unit,
'width_unit': size_unit,
})
return self._send_request('shipping-products', params=params)
def _get_shipping_rate(self, carrier, order=None, picking=None, parcel=None, order_weight=None):
# Get source, destination and weight
if order:
to_country = order.partner_shipping_id.country_id.code
from_country = order.warehouse_id.partner_id.country_id.code
error_lines = order.order_line.filtered(lambda line: not line.product_id.weight and not line.is_delivery and line.product_id.type != 'service' and not line.display_type)
if error_lines:
raise UserError(_("The estimated shipping price cannot be computed because the weight is missing for the following product(s): \n %s", ", ".join(error_lines.product_id.mapped('name'))))
packages = carrier._get_packages_from_order(order, carrier.sendcloud_default_package_type_id)
total_weight = order_weight if order_weight else sum(pack.weight for pack in packages)
elif picking:
to_country = picking.destination_country_code
from_country = picking.location_id.warehouse_id.partner_id.country_id.code
total_weight = float(parcel['weight'])
else:
raise UserError(_('No picking or order provided'))
if not to_country or not from_country:
raise UserError(_('Make sure country codes are set in partner country and warehouse country'))
total_weight = int(carrier.sendcloud_convert_weight(total_weight, grams=True))
if total_weight < carrier.sendcloud_shipping_id.min_weight and order:
raise UserError(_('Order below minimum weight of carrier'))
if parcel:
shipping_methods = [{
'id': parcel.get('shipment', {}).get('id'),
'weight': int(float(parcel.get('weight', 0))*1000), # parcel weight is defined in kg
}]
else:
shipping_methods = self._get_shipping_methods(carrier, from_country, to_country, total_weight=total_weight)
if not shipping_methods or (len(shipping_methods) == 1 and not shipping_methods[0]):
raise UserError(_('There is no shipping method available for this order with the selected carrier'))
packages_no = 1
if order:
default_package = carrier.sendcloud_default_package_type_id
target_weight = default_package.max_weight if default_package else False
target_weight = int(carrier.sendcloud_convert_weight(target_weight, grams=True)) if target_weight else False
packages_no, total_weight = self._split_shipping(carrier.sendcloud_shipping_id, total_weight, target_weight)
if packages_no > 1:
# We're forcefully calling this method from a sale order, as we only want an estimation of the rating, take the 'heaviest' methods
shipping_methods = [m for m in shipping_methods if m['properties']['max_weight'] == carrier.sendcloud_shipping_id.max_weight] # We're sure here there's at least one matching method as max_weight was updated in _get_shipping_methods
shipping_prices = self._get_shipping_prices(shipping_methods, to_country, from_country, total_weight)
if not shipping_prices:
return False
shipping_price = min(shipping_prices.items(), key=lambda p: float(p[1]['price']))
price = float(shipping_price[1].get('price')) * packages_no
currency = shipping_price[1].get('currency')
currency_id = carrier.env['res.currency'].with_context(active_test=False).search([('name', '=', currency)])
if not currency_id:
raise UserError(_('Could not find currency %s', currency))
to_currency_id = order.currency_id if order else picking.sale_id.currency_id
converted_price = currency_id._convert(price, to_currency_id, carrier.env.company, fields.Date.context_today(carrier))
return converted_price, packages_no
def _send_shipment(self, picking, is_return=False):
sender_id = None
if not is_return:
# get warehouse for each picking and get the sender address to use for shipment
sender_id = self._get_pick_sender_address(picking)
parcels = self._prepare_parcel(picking, sender_id, is_return)
# the id of the access_point needs to be passed as parameter following Sendcloud API's
if 'access_point_address' in picking.sale_id and picking.sale_id.access_point_address:
for parcel in parcels:
parcel['to_service_point'] = picking.sale_id.access_point_address['id']
data = {
'parcels': parcels
}
parameters = {
'errors': 'verbose-carrier',
}
res = self._send_request('parcels', 'post', data, params=parameters)
res_parcels = res.get('parcels')
if not res_parcels:
error_message = res['failed_parcels'][0].get('errors', False)
raise UserError(_("Something went wrong, parcel not returned from Sendcloud:\n %s'.", error_message))
return res_parcels
def _track_shipment(self, parcel_id):
parcel = self._send_request(f'parcels/{parcel_id}')
return parcel['parcel']
def _cancel_shipment(self, parcel_id):
res = self._send_request(f'parcels/{parcel_id}/cancel', method='post')
return res
def _get_document(self, url):
''' Returns pdf content of document to print '''
self.logger(f'get {url}', 'sendcloud get document')
try:
res = self.session.request(method='get', url=url, timeout=60)
except Exception as err:
self.logger(str(err), f'sendcloud response {url}')
raise UserError(_('Something went wrong, please try again later!!'))
self.logger(f'{res.content}', 'sendcloud get document response')
if res.status_code != 200:
raise UserError(_('Could not get document!'))
return res.content
def _get_addresses(self):
res = self._send_request('user/addresses/sender')
return res.get('sender_addresses', [])
def _split_shipping(self, shipping_product_id, total_weight, target_weight=False):
# if the weight is greater than max weight and source is order (initial estimate)
# split the weight into packages instead of returning no price / offer
shipping_count = 1
shipping_weight = total_weight
# max weight from sendcloud is 1 gram extra (eg. if max allowed weight = 3000g, sendcloud_shipping_id.max_weight = 3001g)
max_weight = target_weight if target_weight else shipping_product_id.max_weight - 1
if target_weight or total_weight > max_weight:
shipping_count = math.ceil(total_weight / max_weight)
shipping_weight = max_weight
return shipping_count, shipping_weight
def _get_shipping_prices(self, shipping_methods, to_country, from_country, weight=None):
shipping_prices = dict()
params = {
'shipping_method_id': None,
'to_country': to_country,
'from_country': from_country,
'weight': weight,
'weight_unit': 'gram',
}
for shipping_method in shipping_methods:
shipping_id = params['shipping_method_id'] = shipping_method['id']
if not weight:
params['weight'] = shipping_method['properties']['max_weight'] - 1 # the weight of a shipping_method is always in gram
# the API response is an Array of 1 dict with price and currency (usually EUR)
res = self._send_request('shipping-price', params=params)[0]
if res.get('price'):
shipping_prices[shipping_id] = res
elif shipping_id == 8: # Sendcloud Unstamped Letter
# shipping id 8 is a test shipping and does not provide a price, but we still need the flow to continue
# the check is done after the request since in the future if price is actually returned it will be passed correctly
shipping_prices = {8: {'price': 0.0, 'currency': 'EUR'}}
return shipping_prices
def _get_shipping_methods(self, carrier_id, from_country, to_country, total_weight=None, is_return=False, sizes=None):
"""
We're now working with a sendcloud's PRODUCT
We must fetch the differents METHODS in that product, in order to find the most appropriate !
We may however still be in the case where there isn't a single method able to handle the total weight of the order.
In this last case, we may split the shipping in multiple packages as before.
"""
sendcloud_product_id = carrier_id.sendcloud_return_id if is_return else carrier_id.sendcloud_shipping_id
if not sendcloud_product_id:
return None
shipping_code = sendcloud_product_id.sendcloud_code
shipping_carrier = sendcloud_product_id.carrier
# Despite the fact that the sendcloud's documentation says the product
# weight range is inclusive, a search with total_weight == max_weight
# returns no result.
single_shipping = total_weight and total_weight < sendcloud_product_id.max_weight
if single_shipping:
shipping_products = self._get_shipping_products(from_country, is_return=is_return, carrier=shipping_carrier, to_country=to_country, weight=total_weight, sizes=sizes)
else:
shipping_products = self._get_shipping_products(from_country, is_return=is_return, carrier=shipping_carrier, to_country=to_country, sizes=sizes)
shipping_product = next(filter(lambda p: p['code'] == shipping_code, shipping_products), None)
if not shipping_product:
if single_shipping:
# single_shipping may be false-positive due to the local value 'sendcloud_product_id.max_weight'
# we call back this method without filtering the call by weight to reach the update of local cache
return self._get_shipping_methods(carrier_id, from_country, to_country, is_return=is_return, sizes=sizes)
else:
return None
if not single_shipping:
# Update product local values
max_allowed_weight = shipping_product['weight_range']['max_weight'] # This data is only valid if we didn't set the 'weight' arg in _get_shipping_products call
self._validate_shipping_product_max_weight(sendcloud_product_id, max_allowed_weight)
user_filters = None if is_return else carrier_id.sendcloud_product_functionalities
shipping_ids = self._filtered_shipping_method_ids(shipping_product['methods'], user_filters)
if not shipping_ids:
raise UserError(_("There's no shipping method matching all your selected filters for this picking/order."))
return list(filter(lambda m: m['id'] in shipping_ids, shipping_product['methods']))
def _filtered_shipping_method_ids(self, shipping_methods, user_filters):
"""
Apply user filters on methods
for each filter key : [values]
if the ONLY value for a key is None:
then the key MUST NOT be present in the method functions
if there are "other values" AND None:
then OR :
the key IS present AND it's value IN filter's values
the key IS NOT present
if there are ONLY "other values":
the key IS present AND it's value IN filter's values
Default : return all the ids when there's no user_filters
Return : set of id
"""
shipping_ids = {m['id'] for m in shipping_methods}
if not user_filters:
return shipping_ids
for func, options in user_filters.items():
def pass_filter(shipping_method):
return (func not in shipping_method['functionalities'] and 'None' in options) or (func in shipping_method['functionalities'] and shipping_method['functionalities'][func] in options)
filtered_ids = {m['id'] for m in shipping_methods if pass_filter(m)}
if not filtered_ids:
return []
elif not shipping_ids:
shipping_ids = filtered_ids
else:
shipping_ids &= filtered_ids
return shipping_ids
def _send_request(self, endpoint, method='get', data=None, params=None, route=BASE_URL):
url = url_join(route, endpoint)
self.logger(f'{url}\n{method}\n{data}\n{params}', f'sendcloud request {endpoint}')
if method not in ['get', 'post']:
raise Exception(f'Unhandled request method {method}')
try:
res = self.session.request(method=method, url=url, json=data, params=params, timeout=60)
self.logger(f'{res.status_code} {res.text}', f'sendcloud response {endpoint}')
res = res.json()
except Exception as err:
self.logger(str(err), f'sendcloud response {endpoint}')
raise UserError(_('Something went wrong, please try again later!!'))
if 'error' in res:
raise UserError(res['error']['message'])
return res
def _prepare_parcel_items(self, packages, carrier, products_values=None):
if not isinstance(packages, list):
packages = [packages]
if not products_values:
products_values = dict()
parcel_items = {}
for pkg in packages:
for commodity in pkg.commodities:
key = commodity.product_id.id
if key in parcel_items:
parcel_items[key]['quantity'] += commodity.qty
continue
if commodity.product_id.id in products_values:
value = products_values[commodity.product_id.id]['avg_value']
else:
value = commodity.monetary_value
hs_code = commodity.product_id.hs_code or ''
for ch in [' ', '.']:
hs_code = hs_code.replace(ch, '')
parcel_items[key] = {
'description': commodity.product_id.name,
'quantity': commodity.qty,
'weight': float_repr(carrier.sendcloud_convert_weight(commodity.product_id.weight), 3),
'value': round(value, 2),
'hs_code': hs_code[:8],
'origin_country': commodity.country_of_origin or '',
'sku': commodity.product_id.barcode or '',
}
return list(parcel_items.values())
def _get_house_number(self, address):
house_number = re.search(r"(\d+[-\/]?\d* ?[a-zA-Z]?\d*)(?![a-zA-Z])", address)
if house_number:
return house_number.group()
return ' '
def _validate_partner_details(self, partner):
if not partner.phone and not partner.mobile:
raise UserError(_('%(partner_name)s phone required', partner_name=partner.name))
if not partner.email:
raise UserError(_('%(partner_name)s email required', partner_name=partner.name))
if not all([partner.street, partner.city, partner.zip, partner.country_id]):
raise UserError(_('The %s address needs to have the street, city, zip, and country', partner.name))
if (partner.street and len(partner.street) > 75) or (partner.street2 and len(partner.street2) > 75):
raise UserError(_('Each address line can only contain a maximum of 75 characters. You can split the address into multiple lines to try to avoid this limitation.'))
def _validate_shipping_product_max_weight(self, shipping_product_id, fresh_max_weight):
if shipping_product_id.max_weight != fresh_max_weight:
shipping_product_id.max_weight = fresh_max_weight
return False
return True
def _get_max_package_sizes(self, package_ids):
"""
Return the largest size for each dimension of a list of package
:param delivery.carrier self: the Sendcloud delivery carrier
:param list[DeliveryPackage] package_ids: A list of package
:return: A dict of size for each dimension or None if all sizes are 0
:rtype: dict[str, int] or None
"""
sizes = {
"length": 0,
"height": 0,
"width": 0,
}
for pkg in package_ids:
for axis in ("length", "height", "width"):
sizes[axis] = max(sizes[axis], pkg.dimension.get(axis))
return sizes
def _prepare_parcel(self, picking, sender_id, is_return):
# Pre-checks
carrier_id = picking.carrier_id
delivery_packages = carrier_id._get_packages_from_picking(picking, carrier_id.sendcloud_default_package_type_id) # If nothing to return -> Error
if any(not pkg.weight for pkg in delivery_packages):
raise UserError(_("Ensure picking has shipping weight, if using packages, each package should have a shipping weight"))
#Prepare API call and process data
to_partner_id = picking.partner_id
from_partner_id = picking.picking_type_id.warehouse_id.partner_id
if is_return:
to_partner_id, from_partner_id = from_partner_id, to_partner_id
from_country, to_country = from_partner_id.country_id.code, to_partner_id.country_id.code
self._validate_partner_details(to_partner_id)
shipping_weight = int(carrier_id.sendcloud_convert_weight(picking.shipping_weight, grams=True))
to_europe = to_partner_id.country_id.code in to_partner_id.env.ref('base.europe').country_ids.mapped('code')
use_multicollo = carrier_id.sendcloud_use_batch_shipping and to_europe
single_shipping = len(delivery_packages) == 1 or (use_multicollo and len(delivery_packages) <= 20)
api_weight = shipping_weight if single_shipping else None
# Fetch shipping methods compatible with current picking
shipping_methods = self._get_shipping_methods(picking.carrier_id, from_country, to_country, api_weight, is_return)
sendcloud_product_id = carrier_id.sendcloud_return_id if is_return else carrier_id.sendcloud_shipping_id
user_uom_max_weight = carrier_id.sendcloud_convert_weight(sendcloud_product_id.max_weight - 1, grams=True, reverse=True) # grams to user uom
user_weight_uom = carrier_id.env['product.template'].sudo()._get_weight_uom_id_from_ir_config_parameter()
if not shipping_methods:
raise UserError(_('There is no shipping method available for this picking with the selected carrier'))
elif any(float_compare(pkg.weight, user_uom_max_weight, precision_rounding=user_weight_uom.rounding) > 0 for pkg in delivery_packages):
overweight_products = picking.move_ids.filtered(lambda m: float_compare(m.product_id.weight, user_uom_max_weight, precision_rounding=m.product_uom.rounding) > 0)
not_packed = bool(not picking.package_ids and picking.weight_bulk)
if not_packed:
message = _('The total weight of your transfer is too heavy for the heaviest available shipping method.')
else:
message = _('Some packages in your transfer are too heavy for the heaviest available shipping method.')
message += _("\nTry to distribute your products across your packages so that they weigh less than %(max_weight)s %(unit)s or choose another carrier.", max_weight=user_uom_max_weight, unit=user_weight_uom.name)
if overweight_products:
product_moves = ", ".join(overweight_products.mapped('name'))
message += _("""\nAdditionally, some individual product(s) are too heavy for the heaviest available shipping method.
\nDivide the quantity of the following product(s) across your packages if possible or choose another carrier:\n\t%s""", product_moves)
raise UserError(message)
shipping_prices = self._get_shipping_prices(shipping_methods, to_country, from_country)
# Assign consequent price to each method, delete the method if no price is available
for shipping_method in reversed(shipping_methods):
price = shipping_prices.get(shipping_method['id'], {}).get('price')
shipping_method['price'] = float(price) if price else 0.0
method_shipments = self._assign_packages_to_methods(carrier_id, delivery_packages, shipping_methods, use_multicollo)
parcel_common = self._prepare_parcel_common_data(picking, is_return, sender_id)
products_values = self._get_products_values(picking.sale_id)
total_value = 0.0
if picking.sale_id:
total_value = sum(line.price_reduce_taxinc * line.product_uom_qty for line in
picking.sale_id.order_line.filtered(
lambda l: l.product_id.type in ('consu', 'product') and not l.display_type
)
)
else:
total_value = sum([line.product_id.lst_price * line.product_qty for line in picking.move_ids])
total_value = float_repr(total_value, 2)
parcels = []
for shipping in method_shipments: # shipping = { 'id': (int), 'packages': [pkg or [pkg]]}, each id is unique among the whole list
parcel_common['shipment'] = {
'id': shipping['id'],
}
for pkg in shipping['packages']: # pkg is either a single pkg or a list of pkg
parcel = dict(parcel_common)
if isinstance(pkg, list):
max_sizes = self._get_max_package_sizes(pkg)
parcel.update({
'weight': float_repr(sum(p.weight for p in pkg), 3),
'length': max_sizes['length'],
'width': max_sizes['width'],
'height': max_sizes['height'],
'quantity': len(pkg),
})
else:
parcel.update({
'weight': float_repr(pkg.weight, 3),
'length': pkg.dimension['length'],
'width': pkg.dimension['width'],
'height': pkg.dimension['height'],
})
parcel['parcel_items'] = self._prepare_parcel_items(pkg, carrier_id, products_values)
parcel['total_order_value'] = total_value
parcels.append(parcel)
return parcels
def _assign_packages_to_methods(self, carrier_id, delivery_packages, shipping_methods, use_multicollo=False):
sorted_methods = self._get_cheapest_method_by_weight_ranges(shipping_methods)
# Now methods are sorted by price :
# For regular shippings, we can fit packages in the first matching method
# For multicollo :
# Group packages wisely to minimize the cost if there's more than 20 of them
# Fitting of method is done by batch of packages
failed_assignation = []
for package in delivery_packages:
sendcloud_uom_package_weight = int(carrier_id.sendcloud_convert_weight(package.weight, grams=True)) # user uom to grams
cheapest_method = next((m for m in sorted_methods if m['weight']['min'] <= sendcloud_uom_package_weight <= m['weight']['max']), None)
if cheapest_method:
cheapest_method['packages'].append(package)
else:
failed_assignation.append(package)
if failed_assignation:
details = "\n\t- ".join(f"{pkg.name}: {pkg.weight}" for pkg in failed_assignation)
raise UserError(_("There's no method with matching weight range for packages :\n%s\nYou can either choose another carrier, change your filters or redefine the content of your package(s).") % details)
sorted_methods = [m for m in sorted_methods if m['packages']] # Remove methods without package
if not use_multicollo:
return sorted_methods
multicollo_batch = [{
'id': False,
'packages': [[]],
}]
pkg_number = math.ceil(len(delivery_packages)/MULTICOLLO_MAX_PACKAGE)
min_batch_size = len(delivery_packages)%MULTICOLLO_MAX_PACKAGE
max_batch_size = MULTICOLLO_MAX_PACKAGE
for method in reversed(sorted_methods):
while True: # the current method may be set for more than 20pkg, ensure to assign all of them
batch_size = len(multicollo_batch[-1]['packages'][-1])
method_pkg_size = len(method['packages'])
if not multicollo_batch[-1]['id']:
multicollo_batch[-1]['id'] = method['id']
if (batch_size + method_pkg_size) <= max_batch_size:
multicollo_batch[-1]['packages'][-1].extend(method['packages'])
break # Exit the 'infinite' loop, as the else condition always minimize batch_size and method_pkg_size that are defined on basis of finite sets, we will forcefully reach this point
else:
to_fulfill = max_batch_size - batch_size
multicollo_batch[-1]['packages'][-1].extend(method['packages'][:to_fulfill-1])
method['packages'] = method['packages'][to_fulfill:]
pkg_number -= 1
max_batch_size = MULTICOLLO_MAX_PACKAGE
multicollo_batch[-1]['packages'].append([])
if min_batch_size <= len(multicollo_batch[-1]['packages'][-1]): # As we're iterating in reverse order (on price), we want to ship a min. of package in early batches to minimize the cost
pkg_number -= 1
if pkg_number == 0:
break
min_batch_size = (min_batch_size - len(multicollo_batch[-1]['packages'][-1])) % 20
max_batch_size = MULTICOLLO_MAX_PACKAGE
multicollo_batch.append({
'id': False,
'packages': [[]],
})
else:
max_batch_size = min_batch_size # Apply minimum append strategy on next iteration
return multicollo_batch
def _get_products_values(self, sale_order=None):
"""
If the parcel come from a sale order, we take the price from it.
However, as we may have the same product sold at different prices in the same SO,
and as sendcloud is strict (particularly when it comes to customs), we define the average price per product
"""
products_values = dict()
if not sale_order:
return products_values
for line in sale_order.order_line:
if line.product_id.type not in ('consu', 'product') or line.display_type or float_is_zero(line.product_uom_qty, precision_rounding=line.product_uom.rounding):
continue
if line.product_id.id in products_values:
products_values[line.product_id.id]['tot_qty'] += line.product_uom_qty
products_values[line.product_id.id]['tot_value'] += line.price_reduce_taxinc * line.product_uom_qty
else:
products_values[line.product_id.id] = {
'tot_qty': line.product_uom_qty,
'tot_value': line.price_reduce_taxinc * line.product_uom_qty,
}
for val in products_values.values():
val.update({
'avg_value': float(val['tot_value'])/float(val['tot_qty'])
})
return products_values
def _get_cheapest_method_by_weight_ranges(self, shipping_methods):
# order methods by min_weight(1st) and max_weight(2nd) and price(3rd)
shipping_methods = sorted(shipping_methods, key=lambda m: (m['properties']['min_weight'], m['properties']['max_weight'], m['price']))
sorted_methods = []
# Define best method by weight range
for method in shipping_methods:
if sorted_methods and method['properties']['min_weight'] == sorted_methods[-1]['weight']['min'] and method['properties']['max_weight'] == sorted_methods[-1]['weight']['max']:
continue # Due to previous sort, price of current method is forcefully greater for the same weight range (which is worse)
sorted_methods.append({
'id': method['id'],
'price': method['price'],
'weight': {
'min': method['properties']['min_weight'],
'max': method['properties']['max_weight'],
},
'packages': [],
})
sorted_methods = sorted(sorted_methods, key=lambda m: m['price'])
return sorted_methods
def _prepare_parcel_common_data(self, picking, is_return, sender_id=False):
if 'access_point_address' in picking.sale_id and picking.sale_id.access_point_address:
# actual partner data is stored on partner_id's parent, partner_id contains access_point data
if not picking.partner_id.parent_id:
raise UserError(_('The delivery address of the customer has been removed from the pickup location. This information is required by Sendcloud. Please go to the delivery partner via the delivery order and make sure the parent of the delivery partner is the partner you want to ship to.'))
to_partner_id = picking.partner_id.parent_id
else:
to_partner_id = picking.partner_id
from_partner_id = picking.picking_type_id.warehouse_id.partner_id
if is_return:
to_partner_id, from_partner_id = from_partner_id, to_partner_id
carrier_id = picking.carrier_id
apply_rules = carrier_id.sendcloud_shipping_rules
sendcloud_product_id = carrier_id.sendcloud_return_id if is_return else carrier_id.sendcloud_shipping_id
if picking.sale_id:
currency_name = picking.sale_id.currency_id.name
else:
currency_name = picking.company_id.currency_id.name
parcel_common = {
'name': (to_partner_id.name or to_partner_id.parent_id.name or '')[:75],
'company_name': to_partner_id.commercial_company_name[:50] if to_partner_id.commercial_company_name else '',
'address': to_partner_id.street,
'address_2': to_partner_id.street2 or '',
'house_number': self._get_house_number(to_partner_id.street),
'city': to_partner_id.city or '',
'country_state': to_partner_id.state_id.code or '',
'postal_code': to_partner_id.zip,
'country': to_partner_id.country_id.code,
'telephone': to_partner_id.mobile or to_partner_id.phone or '',
'email': to_partner_id.email or '',
'request_label': True,
'apply_shipping_rules': apply_rules,
'is_return': is_return,
'shipping_method_checkout_name': sendcloud_product_id.name,
'order_number': picking.sale_id.name or picking.name,
'customs_shipment_type': 4 if is_return else 2,
'customs_invoice_nr': picking.origin or '',
'total_order_value_currency': currency_name
}
if sender_id:
# "sender_id" implies that "not is_return" (c.f. send_shipment())
# So we're sure here that sender_id and from_partner_id holds the warehouse's address
parcel_common.update({
'sender_address': sender_id
})
elif from_partner_id:
# As we can't use 'sender_address' and 'from_*' fields at the same time in the API call
# we only use from_partner_id in case sender_id is false
self._validate_partner_details(from_partner_id)
parcel_common.update({
'from_name': (from_partner_id.name or from_partner_id.parent_id.name or '')[:75],
'from_company_name': from_partner_id.commercial_company_name[:50] if from_partner_id.commercial_company_name else '',
'from_house_number': self._get_house_number(from_partner_id.street),
'from_address_1': from_partner_id.street or '',
'from_address_2': from_partner_id.street2 or '',
'from_city': from_partner_id.city or '',
'from_state': from_partner_id.state_id.code or '',
'from_postal_code': from_partner_id.zip or '',
'from_country': from_partner_id.country_id.code,
'from_telephone': from_partner_id.mobile or from_partner_id.phone or '',
'from_email': from_partner_id.email or '',
})
return parcel_common
def _get_pick_sender_address(self, picking):
warehouse_name = picking.location_id.warehouse_id.name.lower().replace(' ', '')
addresses = self._get_addresses()
res_id = None
for addr in addresses:
label = addr.get('label', '').lower().replace(' ', '')
contact_name = addr.get('contact_name', '').lower().replace(' ', '')
if warehouse_name in (label, contact_name):
res_id = addr['id']
break
if not res_id:
raise UserError(_('No address found with contact name %s on your sendcloud account.', picking.location_id.warehouse_id.name))
return res_id