208 lines
7.5 KiB
Python
208 lines
7.5 KiB
Python
import http
|
||
import os
|
||
from contextlib import suppress
|
||
from time import sleep
|
||
from typing import Optional
|
||
from urllib.parse import urljoin
|
||
|
||
import requests
|
||
from django.conf import settings
|
||
from django.core.files.base import ContentFile
|
||
|
||
from store.utils import is_migration_running
|
||
|
||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
|
||
|
||
|
||
class CDEKStatus:
|
||
# Принят
|
||
ACCEPTED = "ACCEPTED"
|
||
# Создан
|
||
CREATED = "CREATED"
|
||
# Принят на склад отправителя
|
||
RECEIVED_AT_SHIPMENT_WAREHOUSE = "RECEIVED_AT_SHIPMENT_WAREHOUSE"
|
||
# Выдан на отправку в г. отправителе
|
||
READY_FOR_SHIPMENT_IN_SENDER_CITY = "READY_FOR_SHIPMENT_IN_SENDER_CITY"
|
||
# Возвращен на склад отправителя
|
||
RETURNED_TO_SENDER_CITY_WAREHOUSE = "RETURNED_TO_SENDER_CITY_WAREHOUSE"
|
||
# Сдан перевозчику в г. отправителе
|
||
TAKEN_BY_TRANSPORTER_FROM_SENDER_CITY = "TAKEN_BY_TRANSPORTER_FROM_SENDER_CITY"
|
||
# Отправлен в г. транзит
|
||
SENT_TO_TRANSIT_CITY = "SENT_TO_TRANSIT_CITY"
|
||
# Встречен в г. транзите
|
||
ACCEPTED_IN_TRANSIT_CITY = "ACCEPTED_IN_TRANSIT_CITY"
|
||
# Принят на склад транзита
|
||
ACCEPTED_AT_TRANSIT_WAREHOUSE = "ACCEPTED_AT_TRANSIT_WAREHOUSE"
|
||
# Возвращен на склад транзита
|
||
RETURNED_TO_TRANSIT_WAREHOUSE = "RETURNED_TO_TRANSIT_WAREHOUSE"
|
||
# Выдан на отправку в г. транзите
|
||
READY_FOR_SHIPMENT_IN_TRANSIT_CITY = "READY_FOR_SHIPMENT_IN_TRANSIT_CITY"
|
||
# Сдан перевозчику в г. транзите
|
||
TAKEN_BY_TRANSPORTER_FROM_TRANSIT_CITY = "TAKEN_BY_TRANSPORTER_FROM_TRANSIT_CITY"
|
||
# Отправлен в г. отправитель
|
||
SENT_TO_SENDER_CITY = "SENT_TO_SENDER_CITY"
|
||
# Отправлен в г. получатель
|
||
SENT_TO_RECIPIENT_CITY = "SENT_TO_RECIPIENT_CITY"
|
||
# Встречен в г. отправителе
|
||
ACCEPTED_IN_SENDER_CITY = "ACCEPTED_IN_SENDER_CITY"
|
||
# Встречен в г. получателе
|
||
ACCEPTED_IN_RECIPIENT_CITY = "ACCEPTED_IN_RECIPIENT_CITY"
|
||
# Принят на склад доставки
|
||
ACCEPTED_AT_RECIPIENT_CITY_WAREHOUSE = "ACCEPTED_AT_RECIPIENT_CITY_WAREHOUSE"
|
||
# Принят на склад до востребования
|
||
ACCEPTED_AT_PICK_UP_POINT = "ACCEPTED_AT_PICK_UP_POINT"
|
||
# Выдан на доставку
|
||
TAKEN_BY_COURIER = "TAKEN_BY_COURIER"
|
||
# Возвращен на склад доставки
|
||
RETURNED_TO_RECIPIENT_CITY_WAREHOUSE = "RETURNED_TO_RECIPIENT_CITY_WAREHOUSE"
|
||
# Вручен
|
||
DELIVERED = "DELIVERED"
|
||
# Не вручен
|
||
NOT_DELIVERED = "NOT_DELIVERED"
|
||
# Некорректный заказ
|
||
INVALID = "INVALID"
|
||
# Таможенное оформление в стране отправления
|
||
IN_CUSTOMS_INTERNATIONAL = "IN_CUSTOMS_INTERNATIONAL"
|
||
# Отправлено в страну назначения
|
||
SHIPPED_TO_DESTINATION = "SHIPPED_TO_DESTINATION"
|
||
# Передано транзитному перевозчику
|
||
PASSED_TO_TRANSIT_CARRIER = "PASSED_TO_TRANSIT_CARRIER"
|
||
# Таможенное оформление в стране назначения
|
||
IN_CUSTOMS_LOCAL = "IN_CUSTOMS_LOCAL"
|
||
# Таможенное оформление завершено
|
||
CUSTOMS_COMPLETE = "CUSTOMS_COMPLETE"
|
||
# Заложен в постамат
|
||
POSTOMAT_POSTED = "POSTOMAT_POSTED"
|
||
# Изъят из постамата курьером
|
||
POSTOMAT_SEIZED = "POSTOMAT_SEIZED"
|
||
# Изъят из постамата клиентом
|
||
POSTOMAT_RECEIVED = "POSTOMAT_RECEIVED"
|
||
|
||
|
||
class CDEKClient:
|
||
AUTH_ENDPOINT = 'oauth/token'
|
||
ORDER_INFO_ENDPOINT = 'orders'
|
||
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
|
||
BARCODE_ENDPOINT = 'print/barcodes'
|
||
|
||
MAX_RETRIES = 2
|
||
|
||
def __init__(self, client_id, client_secret, grant_type='client_credentials'):
|
||
self.api_url = 'https://api.cdek.ru/v2/'
|
||
self.client_id = client_id
|
||
self.client_secret = client_secret
|
||
self.grant_type = grant_type
|
||
|
||
self.session = requests.Session()
|
||
|
||
def request(self, method, url, *args, **kwargs):
|
||
joined_url = urljoin(self.api_url, url)
|
||
request = requests.Request(method, joined_url, *args, **kwargs)
|
||
|
||
retries = 0
|
||
while retries < self.MAX_RETRIES:
|
||
retries += 1
|
||
prepared = self.session.prepare_request(request)
|
||
try:
|
||
r = self.session.send(prepared)
|
||
except:
|
||
continue
|
||
|
||
# TODO: handle/log errors
|
||
if r.status_code == http.HTTPStatus.UNAUTHORIZED:
|
||
self.authorize()
|
||
continue
|
||
return r
|
||
|
||
def authorize(self):
|
||
params = {
|
||
'client_id': self.client_id,
|
||
'client_secret': self.client_secret,
|
||
'grant_type': self.grant_type
|
||
}
|
||
r = self.request('POST', self.AUTH_ENDPOINT, params=params)
|
||
if r:
|
||
data = r.json()
|
||
token = data['access_token']
|
||
self.session.headers.update({'Authorization': f'Bearer {token}'})
|
||
|
||
def get_order_info(self, im_number):
|
||
params = {
|
||
'im_number': str(im_number)
|
||
}
|
||
return self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
|
||
|
||
def create_order(self, order_data):
|
||
return self.request('POST', self.ORDER_INFO_ENDPOINT, json=order_data)
|
||
|
||
def edit_order(self, order_data):
|
||
return self.request('PATCH', self.ORDER_INFO_ENDPOINT, json=order_data)
|
||
|
||
def calculate_tariff(self, data):
|
||
return self.request('POST', self.CALCULATOR_TARIFF_ENDPOINT, json=data)
|
||
|
||
def generate_barcode(self, cdek_number, format="A6") -> Optional[str]:
|
||
request_data = {
|
||
"orders": [{"cdek_number": cdek_number}],
|
||
"copy_count": 1,
|
||
"format": format
|
||
}
|
||
|
||
r = self.request('POST', self.BARCODE_ENDPOINT, json=request_data)
|
||
if not r:
|
||
return None
|
||
|
||
resp_data = r.json()
|
||
if 'entity' not in resp_data:
|
||
return None
|
||
|
||
barcode_uuid = resp_data['entity']['uuid']
|
||
return barcode_uuid
|
||
|
||
def get_barcode_url(self, uuid) -> Optional[str]:
|
||
if not uuid:
|
||
return None
|
||
|
||
r = self.request('GET', f'{self.BARCODE_ENDPOINT}/{uuid}')
|
||
if not r:
|
||
return None
|
||
|
||
resp_data = r.json()
|
||
if 'entity' not in resp_data:
|
||
return None
|
||
|
||
url = resp_data['entity'].get('url')
|
||
return url
|
||
|
||
def get_barcode_file(self, cdek_number):
|
||
uuid = self.generate_barcode(cdek_number)
|
||
sleep(2) # Sometimes url are not yet created, so be prepared for this
|
||
url = self.get_barcode_url(uuid)
|
||
if not url:
|
||
return None
|
||
|
||
r = self.request('GET', url)
|
||
return ContentFile(r.content) if r and r.content else None
|
||
|
||
def get_order_statuses(self, cdek_number):
|
||
params = {
|
||
'cdek_number': str(cdek_number)
|
||
}
|
||
|
||
r = self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
|
||
if not r:
|
||
return []
|
||
|
||
with suppress(KeyError):
|
||
statuses = r.json()['entity']['statuses']
|
||
statuses = [s.get('code') for s in statuses]
|
||
return statuses
|
||
|
||
return []
|
||
|
||
|
||
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
|
||
|
||
if not is_migration_running():
|
||
client.authorize()
|