+ Update CDEK status in background
This commit is contained in:
Phil Zhitnikov 2023-12-02 17:11:08 +04:00
parent e5e93ab6d5
commit 9d7e45cd65
10 changed files with 193 additions and 0 deletions

View File

@ -25,5 +25,7 @@ chmod-socket = 664
# clear environment on exit # clear environment on exit
vacuum = true vacuum = true
smart-attach-daemon = /tmp/celery-main.pid /var/www/phzhik-poizonstore/run_celery.sh
env = LANG=C.UTF-8 env = LANG=C.UTF-8
enable-threads = true enable-threads = true

View File

@ -1,5 +1,6 @@
import http import http
import os import os
from contextlib import suppress
from time import sleep from time import sleep
from typing import Optional from typing import Optional
from urllib.parse import urljoin from urllib.parse import urljoin
@ -13,6 +14,71 @@ from store.utils import is_migration_running
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings') 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: class CDEKClient:
AUTH_ENDPOINT = 'oauth/token' AUTH_ENDPOINT = 'oauth/token'
ORDER_INFO_ENDPOINT = 'orders' ORDER_INFO_ENDPOINT = 'orders'
@ -118,6 +184,22 @@ class CDEKClient:
r = self.request('GET', url) r = self.request('GET', url)
return ContentFile(r.content) if r and r.content else None 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) client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)

View File

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

22
poizonstore/celery.py Normal file
View File

@ -0,0 +1,22 @@
import os
from datetime import timedelta
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
app = Celery('poizonstore')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
app.conf.beat_schedule = {
'update-cdek-status-every-hour': {
'task': 'store.tasks.schedule_cdek_status_update',
'schedule': timedelta(hours=1),
},
}
@app.task()
def debug_task():
print(f'Task complete')

View File

@ -215,3 +215,12 @@ if not DEBUG:
# We recommend adjusting this value in production. # We recommend adjusting this value in production.
profiles_sample_rate=1.0, profiles_sample_rate=1.0,
) )
# Celery
BROKER_URL = 'redis://localhost:6379/1'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE

View File

@ -9,6 +9,11 @@ djoser==2.2.0
drf-extra-fields==3.5.0 drf-extra-fields==3.5.0
Pillow==9.5.0 Pillow==9.5.0
# Tasks
celery==5.3.6
redis==5.0.1
flower==2.0.1
# Misc # Misc
tqdm==4.65.0 tqdm==4.65.0
django-debug-toolbar==4.1.0 django-debug-toolbar==4.1.0

20
run_celery.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
PROJECT_NAME="poizonstore"
# Run Celery worker
echo 'Starting Celery worker'
celery -A $PROJECT_NAME worker -l INFO --pidfile=/tmp/celery.pid &
# Wait for worker to start
until timeout -t 10 celery -A project inspect ping; do
>&2 echo "Celery workers not available"
done
# Run flower for Celery management
echo 'Starting Celery flower'
celery -A $PROJECT_NAME flower --pidfile=/tmp/celery-flower.pid &
# Run celery beat for periodic tasks
echo 'Starting Celery beat'
celery -A $PROJECT_NAME beat -l INFO --pidfile=/tmp/celery-beat.pid &

3
stop_celery.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
killall celery

View File

@ -355,6 +355,7 @@ class Checklist(models.Model):
COMPLETED = "completed" COMPLETED = "completed"
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED) PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
CHOICES = ( CHOICES = (
(DRAFT, 'Черновик'), (DRAFT, 'Черновик'),

46
store/tasks.py Normal file
View File

@ -0,0 +1,46 @@
from celery import shared_task
from django.db.models import Q
from external_api.cdek import client as cdek_client, CDEKStatus
from .models import Checklist
@shared_task
def check_cdek_status(order_id):
obj = Checklist.objects.filter(id=order_id).first()
if obj is None or obj.cdek_tracking is None or obj.status == Checklist.Status.COMPLETED:
return
# Get CDEK statuses
statuses = cdek_client.get_order_statuses(obj.cdek_tracking)
if not statuses:
return
new_status = obj.status
if CDEKStatus.DELIVERED in statuses:
new_status = Checklist.Status.COMPLETED
elif CDEKStatus.READY_FOR_SHIPMENT_IN_SENDER_CITY in statuses:
new_status = Checklist.Status.CDEK
# Update status
if obj.status != new_status:
print(f'Order [{obj.id}] status: {obj.status} -> {new_status}')
obj.status = new_status
obj.save()
return new_status
@shared_task
def schedule_cdek_status_update():
qs = Checklist.objects.filter(
Q(cdek_tracking__isnull=False) & Q(status__in=Checklist.Status.CDEK_READY_STATUSES)
)
order_count = len(qs)
print(f'Scheduled to update {order_count} orders')
# Spawn a sub-task for every order
for obj in qs:
check_cdek_status.delay(order_id=obj.id)
return order_count