+ 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
vacuum = true
smart-attach-daemon = /tmp/celery-main.pid /var/www/phzhik-poizonstore/run_celery.sh
env = LANG=C.UTF-8
enable-threads = true

View File

@ -1,5 +1,6 @@
import http
import os
from contextlib import suppress
from time import sleep
from typing import Optional
from urllib.parse import urljoin
@ -13,6 +14,71 @@ 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'
@ -118,6 +184,22 @@ class CDEKClient:
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)

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.
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
Pillow==9.5.0
# Tasks
celery==5.3.6
redis==5.0.1
flower==2.0.1
# Misc
tqdm==4.65.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"
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
CHOICES = (
(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