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 poizonstore.utils import deep_get from store.models import Checklist 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 CDEKWebhookTypes: ORDER_STATUS = "ORDER_STATUS" CDEK_STATUS_TO_ORDER_STATUS = { CDEKStatus.DELIVERED: Checklist.Status.COMPLETED, CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY: Checklist.Status.CDEK, } class CDEKClient: AUTH_ENDPOINT = 'oauth/token' ORDER_INFO_ENDPOINT = 'orders' CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff' CALCULATOR_TARIFF_LIST_ENDPOINT = 'calculator/tarifflist' BARCODE_ENDPOINT = 'print/barcodes' WEBHOOK_ENDPOINT = 'webhooks' 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, timeout=settings.EXTERNAL_API_TIMEOUT_SEC) 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 calculate_tarifflist(self, data): return self.request('POST', self.CALCULATOR_TARIFF_LIST_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 [] def setup_webhooks(self): if not settings.SITE_URL: return request_data = { "type": CDEKWebhookTypes.ORDER_STATUS, "url": f"{settings.SITE_URL}/cdek/webhook/{settings.CDEK_WEBHOOK_URL_SALT}/" } return self.request('POST', self.WEBHOOK_ENDPOINT, json=request_data) @staticmethod def process_orderstatus_webhook(data) -> tuple: """ Unpack CDEK request to data. Info: https://api-docs.cdek.ru/29924139.html """ cdek_number = deep_get(data, "attributes", "cdek_number") cdek_status = deep_get(data, "attributes", "code") return cdek_number, cdek_status client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET) if not is_migration_running(): client.authorize() client.setup_webhooks()