first commit
This commit is contained in:
362
modules/stock_package_shipping_sendcloud/carrier.py
Normal file
362
modules/stock_package_shipping_sendcloud/carrier.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
import requests
|
||||
|
||||
import trytond.config as config
|
||||
from trytond.cache import Cache
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.protocols.wrappers import HTTPStatus
|
||||
from trytond.pyson import Eval, If
|
||||
|
||||
from .exceptions import SendcloudCredentialWarning, SendcloudError
|
||||
|
||||
SENDCLOUD_API_URL = 'https://panel.sendcloud.sc/api/v2/'
|
||||
HEADERS = {
|
||||
'Sendcloud-Partner-Id': '03c1facb-63da-4bb1-889c-192fc91ec4e6',
|
||||
}
|
||||
|
||||
|
||||
def sendcloud_api(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
nb_tries, error_message = 0, ''
|
||||
try:
|
||||
while nb_tries < 5:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
error_message = e.args[0]
|
||||
nb_tries += 1
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise
|
||||
except requests.HTTPError as e:
|
||||
error_message = e.args[0]
|
||||
raise SendcloudError(
|
||||
gettext('stock_package_shipping_sendcloud'
|
||||
'.msg_sendcloud_webserver_error',
|
||||
message=error_message))
|
||||
return wrapper
|
||||
|
||||
|
||||
class CredentialSendcloud(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
__name__ = 'carrier.credential.sendcloud'
|
||||
|
||||
company = fields.Many2One('company.company', "Company")
|
||||
public_key = fields.Char("Public Key", required=True, strip=False)
|
||||
secret_key = fields.Char("Secret Key", required=True, strip=False)
|
||||
|
||||
addresses = fields.One2Many(
|
||||
'carrier.sendcloud.address', 'sendcloud', "Addresses",
|
||||
states={
|
||||
'readonly': ~Eval('id') | (Eval('id', -1) < 0),
|
||||
})
|
||||
shipping_methods = fields.One2Many(
|
||||
'carrier.sendcloud.shipping_method', 'sendcloud', "Methods",
|
||||
states={
|
||||
'readonly': ~Eval('id') | (Eval('id', -1) < 0),
|
||||
})
|
||||
|
||||
_addresses_sender_cache = Cache(
|
||||
'carrier.credential.sendcloud.addresses_sender',
|
||||
duration=config.getint(
|
||||
'stock_package_shipping_sendcloud', 'addresses_cache',
|
||||
default=15 * 60),
|
||||
context=False)
|
||||
_shiping_methods_cache = Cache(
|
||||
'carrier.credential.sendcloud.shipping_methods',
|
||||
duration=config.getint(
|
||||
'stock_package_shipping_sendcloud', 'shipping_methods_cache',
|
||||
default=60 * 60))
|
||||
|
||||
@property
|
||||
def auth(self):
|
||||
return self.public_key, self.secret_key
|
||||
|
||||
@property
|
||||
@sendcloud_api
|
||||
def addresses_sender(self):
|
||||
addresses = self._addresses_sender_cache.get(self.id)
|
||||
if addresses is not None:
|
||||
return addresses
|
||||
timeout = config.getfloat(
|
||||
'stock_package_shipping_sendcloud', 'requests_timeout',
|
||||
default=300)
|
||||
response = requests.get(
|
||||
SENDCLOUD_API_URL + 'user/addresses/sender',
|
||||
auth=self.auth, timeout=timeout, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
addresses = response.json()['sender_addresses']
|
||||
self._addresses_sender_cache.set(self.id, addresses)
|
||||
return addresses
|
||||
|
||||
def get_sender_address(self, shipment_or_warehouse, pattern=None):
|
||||
pattern = pattern.copy() if pattern is not None else {}
|
||||
if shipment_or_warehouse.__name__ == 'stock.location':
|
||||
warehouse = shipment_or_warehouse
|
||||
pattern['warehouse'] = warehouse.id
|
||||
else:
|
||||
shipment = shipment_or_warehouse
|
||||
pattern['warehouse'] = shipment.shipping_warehouse.id
|
||||
for address in self.addresses:
|
||||
if address.match(pattern):
|
||||
return int(address.address) if address.address else None
|
||||
|
||||
@sendcloud_api
|
||||
def get_shipping_methods(
|
||||
self, sender_address=None, service_point=None, is_return=False):
|
||||
key = (self.id, sender_address, service_point, is_return)
|
||||
methods = self._shiping_methods_cache.get(key)
|
||||
if methods is not None:
|
||||
return methods
|
||||
params = {}
|
||||
if sender_address:
|
||||
params['sender_address'] = sender_address
|
||||
if service_point:
|
||||
params['service_point'] = service_point
|
||||
if is_return:
|
||||
params['is_return'] = is_return
|
||||
timeout = config.getfloat(
|
||||
'stock_package_shipping_sendcloud', 'requests_timeout',
|
||||
default=300)
|
||||
response = requests.get(
|
||||
SENDCLOUD_API_URL + 'shipping_methods', params=params,
|
||||
auth=self.auth, timeout=timeout, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
methods = response.json()['shipping_methods']
|
||||
self._shiping_methods_cache.set(key, methods)
|
||||
return methods
|
||||
|
||||
def get_shipping_method(self, shipment, package=None):
|
||||
pattern = self._get_shipping_method_pattern(shipment, package=package)
|
||||
for method in self.shipping_methods:
|
||||
if method.match(pattern):
|
||||
if method.shipping_method:
|
||||
return int(method.shipping_method)
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_shipping_method_pattern(cls, shipment, package=None):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
ModelData = pool.get('ir.model.data')
|
||||
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
|
||||
|
||||
if package:
|
||||
weight = UoM.compute_qty(
|
||||
package.weight_uom, package.total_weight, kg, round=False)
|
||||
else:
|
||||
weight = UoM.compute_qty(
|
||||
shipment.weight_uom, shipment.weight, kg, round=False)
|
||||
return {
|
||||
'carrier': shipment.carrier.id if shipment.carrier else None,
|
||||
'weight': weight,
|
||||
}
|
||||
|
||||
@sendcloud_api
|
||||
def get_parcel(self, id):
|
||||
timeout = config.getfloat(
|
||||
'stock_package_shipping_sendcloud', 'requests_timeout',
|
||||
default=300)
|
||||
response = requests.get(
|
||||
SENDCLOUD_API_URL + 'parcels/%s' % id,
|
||||
auth=self.auth, timeout=timeout, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
return response.json()['parcel']
|
||||
|
||||
@sendcloud_api
|
||||
def create_parcels(self, parcels):
|
||||
timeout = config.getfloat(
|
||||
'stock_package_shipping_sendcloud', 'requests_timeout',
|
||||
default=300)
|
||||
response = requests.post(
|
||||
SENDCLOUD_API_URL + 'parcels', json={'parcels': parcels},
|
||||
auth=self.auth, timeout=timeout, headers=HEADERS)
|
||||
if response.status_code == 400:
|
||||
msg = response.json()['error']['message']
|
||||
raise requests.HTTPError(msg, response=response)
|
||||
response.raise_for_status()
|
||||
return response.json()['parcels']
|
||||
|
||||
@sendcloud_api
|
||||
def get_label(self, url):
|
||||
timeout = config.getfloat(
|
||||
'stock_package_shipping_sendcloud', 'requests_timeout',
|
||||
default=300)
|
||||
response = requests.get(
|
||||
url, auth=self.auth, timeout=timeout, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
@classmethod
|
||||
def check_modification(
|
||||
cls, mode, credentials, values=None, external=False):
|
||||
pool = Pool()
|
||||
Warning = pool.get('res.user.warning')
|
||||
super().check_modification(
|
||||
mode, credentials, values=values, external=external)
|
||||
if (mode == 'write'
|
||||
and external
|
||||
and values.keys() & {'public_key', 'secret_key'}):
|
||||
warning_name = Warning.format(
|
||||
'sendcloud_credential', credentials)
|
||||
if Warning.check(warning_name):
|
||||
raise SendcloudCredentialWarning(
|
||||
warning_name,
|
||||
gettext('stock_package_shipping_sendcloud'
|
||||
'.msg_sendcloud_credential_modified'))
|
||||
|
||||
|
||||
class SendcloudAddress(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
__name__ = 'carrier.sendcloud.address'
|
||||
|
||||
sendcloud = fields.Many2One(
|
||||
'carrier.credential.sendcloud', "Sendcloud", required=True)
|
||||
warehouse = fields.Many2One(
|
||||
'stock.location', "Warehouse",
|
||||
domain=[
|
||||
('type', '=', 'warehouse'),
|
||||
])
|
||||
address = fields.Selection(
|
||||
'get_addresses', "Address",
|
||||
help="Leave empty for the Sendcloud default.")
|
||||
|
||||
@fields.depends('sendcloud', '_parent_sendcloud.id')
|
||||
def get_addresses(self):
|
||||
addresses = [('', "")]
|
||||
if (self.sendcloud
|
||||
and self.sendcloud.id is not None
|
||||
and self.sendcloud.id >= 0):
|
||||
for address in self.sendcloud.addresses_sender:
|
||||
addresses.append(
|
||||
(str(address['id']), self._format_address(address)))
|
||||
return addresses
|
||||
|
||||
@classmethod
|
||||
def _format_address(cls, address):
|
||||
return ', '.join(
|
||||
filter(None, [
|
||||
address.get('company_name'),
|
||||
address.get('street'),
|
||||
address.get('house_number'),
|
||||
address.get('postal_code'),
|
||||
address.get('city'),
|
||||
address.get('country')]))
|
||||
|
||||
|
||||
class SendcloudShippingMethod(
|
||||
sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
||||
__name__ = 'carrier.sendcloud.shipping_method'
|
||||
|
||||
sendcloud = fields.Many2One(
|
||||
'carrier.credential.sendcloud', "Sendcloud", required=True)
|
||||
carrier = fields.Many2One(
|
||||
'carrier', "Carrier",
|
||||
domain=[
|
||||
('shipping_service', '=', 'sendcloud'),
|
||||
])
|
||||
warehouse = fields.Many2One(
|
||||
'stock.location', "Warehouse",
|
||||
domain=[
|
||||
('type', '=', 'warehouse'),
|
||||
])
|
||||
min_weight = fields.Float(
|
||||
"Minimal Weight",
|
||||
domain=[
|
||||
['OR',
|
||||
('min_weight', '=', None),
|
||||
('min_weight', '>', 0),
|
||||
],
|
||||
If(Eval('max_weight', 0),
|
||||
('min_weight', '<=', Eval('max_weight', 0)),
|
||||
()),
|
||||
],
|
||||
help="Minimal weight included in kg.")
|
||||
max_weight = fields.Float(
|
||||
"Maximal Weight",
|
||||
domain=[
|
||||
['OR',
|
||||
('max_weight', '=', None),
|
||||
('max_weight', '>', 0),
|
||||
],
|
||||
If(Eval('min_weight', 0),
|
||||
('max_weight', '>=', Eval('min_weight', 0)),
|
||||
()),
|
||||
],
|
||||
help="Maximal weight included in kg.")
|
||||
shipping_method = fields.Selection(
|
||||
'get_shipping_methods', "Shipping Method")
|
||||
|
||||
@fields.depends('sendcloud', 'warehouse', '_parent_sendcloud.id')
|
||||
def get_shipping_methods(self, pattern=None):
|
||||
methods = [(None, '')]
|
||||
if (self.sendcloud
|
||||
and self.sendcloud.id is not None
|
||||
and self.sendcloud.id >= 0):
|
||||
if self.warehouse:
|
||||
sender_address = self.sendcloud.get_sender_address(
|
||||
self.warehouse, pattern=pattern)
|
||||
else:
|
||||
sender_address = None
|
||||
methods += [
|
||||
(str(m['id']), m['name'])
|
||||
for m in self.sendcloud.get_shipping_methods(
|
||||
sender_address=sender_address)]
|
||||
return methods
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
pattern = pattern.copy()
|
||||
if (weight := pattern.pop('weight')) is not None:
|
||||
min_weight = self.min_weight or 0
|
||||
max_weight = self.max_weight or weight
|
||||
if not (min_weight <= weight <= max_weight):
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
|
||||
class Carrier(metaclass=PoolMeta):
|
||||
__name__ = 'carrier'
|
||||
|
||||
sendcloud_format = fields.Selection([
|
||||
('normal 0', "A4 - Top left"),
|
||||
('normal 1', "A4 - Top right"),
|
||||
('normal 2', "A4 - Bottom left"),
|
||||
('normal 3', "A4 - Bottom right"),
|
||||
('label', "A6 - Full page"),
|
||||
], "Format",
|
||||
states={
|
||||
'invisible': Eval('shipping_service') != 'sendcloud',
|
||||
'required': Eval('shipping_service') == 'sendcloud',
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.shipping_service.selection.append(('sendcloud', "Sendcloud"))
|
||||
|
||||
@classmethod
|
||||
def default_sendcloud_format(cls):
|
||||
return 'label'
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
return super().view_attributes() + [
|
||||
("/form/separator[@id='sendcloud']", 'states', {
|
||||
'invisible': Eval('shipping_service') != 'sendcloud',
|
||||
}),
|
||||
]
|
||||
|
||||
@property
|
||||
def shipping_label_mimetype(self):
|
||||
mimetype = super().shipping_label_mimetype
|
||||
if self.shipping_service == 'sendcloud':
|
||||
mimetype = 'application/pdf'
|
||||
return mimetype
|
||||
Reference in New Issue
Block a user