Files
tryton/modules/stock_package_shipping_sendcloud/carrier.py
T

361 lines
13 KiB
Python
Raw Normal View History

2021-08-21 19:38:04 +02:00
# 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
2021-12-11 17:58:44 +01:00
2021-08-21 19:38:04 +02:00
import requests
2025-10-07 08:19:57 +02:00
import trytond.config as config
2021-08-21 19:38:04 +02:00
from trytond.cache import Cache
2021-12-11 17:58:44 +01:00
from trytond.i18n import gettext
2021-08-21 19:38:04 +02:00
from trytond.model import (
2021-12-11 17:58:44 +01:00
MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
2024-03-25 14:24:55 +01:00
from trytond.pool import Pool, PoolMeta
2022-12-31 00:05:40 +01:00
from trytond.protocols.wrappers import HTTPStatus
from trytond.pyson import Eval, If
2021-08-21 19:38:04 +02:00
2024-03-25 14:24:55 +01:00
from .exceptions import SendcloudCredentialWarning, SendcloudError
2021-08-21 19:38:04 +02:00
SENDCLOUD_API_URL = 'https://panel.sendcloud.sc/api/v2/'
2023-05-19 17:32:03 +02:00
HEADERS = {
'Sendcloud-Partner-Id': '03c1facb-63da-4bb1-889c-192fc91ec4e6',
}
2021-08-21 19:38:04 +02:00
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)
2024-07-03 19:46:50 +02:00
else:
raise
2021-08-21 19:38:04 +02:00
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)
2021-08-21 19:38:04 +02:00
addresses = fields.One2Many(
'carrier.sendcloud.address', 'sendcloud', "Addresses",
states={
'readonly': ~Eval('id') | (Eval('id', -1) < 0),
})
2021-08-21 19:38:04 +02:00
shipping_methods = fields.One2Many(
'carrier.sendcloud.shipping_method', 'sendcloud', "Methods",
states={
'readonly': ~Eval('id') | (Eval('id', -1) < 0),
})
2021-08-21 19:38:04 +02:00
_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
2025-10-07 08:19:57 +02:00
timeout = config.getfloat(
'stock_package_shipping_sendcloud', 'requests_timeout',
default=300)
2021-08-21 19:38:04 +02:00
response = requests.get(
SENDCLOUD_API_URL + 'user/addresses/sender',
2025-10-07 08:19:57 +02:00
auth=self.auth, timeout=timeout, headers=HEADERS)
2021-08-21 19:38:04 +02:00
response.raise_for_status()
addresses = response.json()['sender_addresses']
return self._addresses_sender_cache.set(self.id, addresses)
2021-08-21 19:38:04 +02:00
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
2021-08-21 19:38:04 +02:00
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
2025-10-07 08:19:57 +02:00
timeout = config.getfloat(
'stock_package_shipping_sendcloud', 'requests_timeout',
default=300)
2021-08-21 19:38:04 +02:00
response = requests.get(
SENDCLOUD_API_URL + 'shipping_methods', params=params,
2025-10-07 08:19:57 +02:00
auth=self.auth, timeout=timeout, headers=HEADERS)
2021-08-21 19:38:04 +02:00
response.raise_for_status()
methods = response.json()['shipping_methods']
return self._shiping_methods_cache.set(key, methods)
2021-08-21 19:38:04 +02:00
def get_shipping_method(self, shipment, package=None):
pattern = self._get_shipping_method_pattern(shipment, package=package)
2021-08-21 19:38:04 +02:00
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)
2021-08-21 19:38:04 +02:00
return {
'carrier': shipment.carrier.id if shipment.carrier else None,
'weight': weight,
2021-08-21 19:38:04 +02:00
}
@sendcloud_api
def get_parcel(self, id):
2025-10-07 08:19:57 +02:00
timeout = config.getfloat(
'stock_package_shipping_sendcloud', 'requests_timeout',
default=300)
2021-08-21 19:38:04 +02:00
response = requests.get(
SENDCLOUD_API_URL + 'parcels/%s' % id,
2025-10-07 08:19:57 +02:00
auth=self.auth, timeout=timeout, headers=HEADERS)
2021-08-21 19:38:04 +02:00
response.raise_for_status()
return response.json()['parcel']
@sendcloud_api
def create_parcels(self, parcels):
2025-10-07 08:19:57 +02:00
timeout = config.getfloat(
'stock_package_shipping_sendcloud', 'requests_timeout',
default=300)
2021-08-21 19:38:04 +02:00
response = requests.post(
SENDCLOUD_API_URL + 'parcels', json={'parcels': parcels},
2025-10-07 08:19:57 +02:00
auth=self.auth, timeout=timeout, headers=HEADERS)
2025-12-18 15:18:53 +01:00
if response.status_code == HTTPStatus.BAD_REQUEST:
2021-08-21 19:38:04 +02:00
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):
2025-10-07 08:19:57 +02:00
timeout = config.getfloat(
'stock_package_shipping_sendcloud', 'requests_timeout',
default=300)
2023-05-19 17:32:03 +02:00
response = requests.get(
2025-10-07 08:19:57 +02:00
url, auth=self.auth, timeout=timeout, headers=HEADERS)
2021-08-21 19:38:04 +02:00
response.raise_for_status()
return response.content
2024-03-25 14:24:55 +01:00
@classmethod
def check_modification(
cls, mode, credentials, values=None, external=False):
2024-03-25 14:24:55 +01:00
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'))
2024-03-25 14:24:55 +01:00
2021-08-21 19:38:04 +02:00
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.")
2021-08-21 19:38:04 +02:00
shipping_method = fields.Selection(
'get_shipping_methods', "Shipping Method")
@fields.depends('sendcloud', 'warehouse', '_parent_sendcloud.id')
def get_shipping_methods(self, pattern=None):
2021-08-21 19:38:04 +02:00
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
2021-08-21 19:38:04 +02:00
methods += [
(str(m['id']), m['name'])
for m in self.sendcloud.get_shipping_methods(
sender_address=sender_address)]
2021-08-21 19:38:04 +02:00
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)
2021-08-21 19:38:04 +02:00
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',
})
2021-08-21 19:38:04 +02:00
@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):
2025-04-04 00:45:18 +02:00
return super().view_attributes() + [
("/form/separator[@id='sendcloud']", 'states', {
'invisible': Eval('shipping_service') != 'sendcloud',
}),
]
2021-10-11 18:50:11 +02:00
@property
def shipping_label_mimetype(self):
mimetype = super().shipping_label_mimetype
if self.shipping_service == 'sendcloud':
mimetype = 'application/pdf'
return mimetype