+ statistics by clients

+ cdek_tracking
+ checklist create API
+ some permissions
* Cleanup
This commit is contained in:
Phil Zhitnikov 2023-07-05 02:19:58 +04:00
parent 369dbf31ae
commit 4511a65b7f
7 changed files with 149 additions and 102 deletions

View File

@ -1,6 +1,7 @@
from django.contrib import admin
from django.contrib.admin import display
from .models import Category, Checklist, GlobalSettings, PaymentMethod, PromoCode, User, Image
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, User, Image
@admin.register(User)
@ -21,8 +22,12 @@ class ImageAdmin(admin.ModelAdmin):
@admin.register(Checklist)
class ChecklistAdmin(admin.ModelAdmin):
list_display = ('id', 'date', 'price_rub', 'commission_rub')
filter_horizontal = ('images',)
list_display = ('id', 'brand', 'model', 'price_rub', 'commission_rub', 'date', 'status_display')
ordering = ('-status_updated_at', '-created_at')
@display(description='Статус')
def status_display(self, obj: Checklist):
return obj.get_status_display()
def date(self, obj: Checklist):
return obj.status_updated_at or obj.created_at
@ -41,7 +46,7 @@ class PaymentMethodAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
@admin.register(PromoCode)
@admin.register(Promocode)
class PromoCodeAdmin(admin.ModelAdmin):
list_display = ('name', 'discount', 'free_delivery', 'no_comission')

View File

@ -3,6 +3,8 @@ from rest_framework.exceptions import APIException
class CRMException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
def __init__(self, detail=None):
if detail is None:
detail = self.default_detail

View File

@ -130,12 +130,15 @@ class User(AbstractUser):
return concat_not_null_values(self.last_name, self.first_name, self.middle_name)
class PromoCode(models.Model):
class Promocode(models.Model):
name = models.CharField('Название', max_length=100, unique=True)
discount = models.DecimalField('Скидка', max_digits=10, decimal_places=2,)
free_delivery = models.BooleanField('Бесплатная доставка', default=False) # freedelivery
no_comission = models.BooleanField('Без комиссии', default=False) # nocomission
def __str__(self):
return self.name
class Meta:
verbose_name = 'Промокод'
verbose_name_plural = 'Промокоды'
@ -155,9 +158,11 @@ class PaymentMethod(models.Model):
return self.name
@cleanup.select
class Image(models.Model):
image = models.ImageField(upload_to='checklist_images')
is_preview = models.BooleanField(default=False)
checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE, related_name='images')
class Meta:
verbose_name = 'Изображение'
@ -168,6 +173,8 @@ class Image(models.Model):
def generate_checklist_id():
""" Generate unique id for Checklist """
all_ids = Checklist.objects.all().values_list('id', flat=True)
allowed_chars = string.ascii_letters + string.digits
@ -226,18 +233,6 @@ class Checklist(models.Model):
(COMPLETED, 'Завершен'),
)
# Payment types
class PaymentType:
ALFA = "alfa"
TINKOFF = "tink"
RAIFFEISEN = "raif"
CHOICES = (
(ALFA, 'Альфа-Банк'),
(TINKOFF, 'Тинькофф Банк'),
(RAIFFEISEN, 'Райффайзен Банк'),
)
# Delivery
class DeliveryType:
PICKUP = "pickup"
@ -249,7 +244,7 @@ class Checklist(models.Model):
)
created_at = models.DateTimeField(auto_now_add=True)
status_updated_at = models.DateTimeField('Дата обновления статуса заказа')
status_updated_at = models.DateTimeField('Дата обновления статуса заказа', auto_now_add=True)
id = models.CharField(primary_key=True, max_length=settings.CHECKLIST_ID_LENGTH,
default=generate_checklist_id, editable=False)
status = models.CharField('Статус заказа', max_length=15, choices=Status.CHOICES, default=Status.NEW)
@ -263,14 +258,12 @@ class Checklist(models.Model):
model = models.CharField('Модель', max_length=100, null=True, blank=True)
size = models.CharField('Размер', max_length=30, null=True, blank=True)
images = models.ManyToManyField('Image', verbose_name='Картинки', blank=True)
# curencycurency2
price_yuan = models.DecimalField('Цена в юанях', max_digits=10, decimal_places=2, default=0)
# TODO: replace by parser
# TODO: replace real_price by parser
real_price = models.DecimalField('Реальная цена', max_digits=10, decimal_places=2, null=True, blank=True)
# TODO: choose from PromoCode table
# TODO: choose from Promocode table
# promo
promocode = models.CharField('Промокод', max_length=100, null=True, blank=True)
comment = models.CharField('Комментарий', max_length=200, null=True, blank=True)
@ -296,7 +289,8 @@ class Checklist(models.Model):
delivery = models.CharField('Тип доставки', max_length=10, choices=DeliveryType.CHOICES, null=True, blank=True)
# trackid
track_number = models.CharField('Трек-номер', max_length=100, null=True, blank=True)
poizon_tracking = models.CharField('Трек-номер Poizon', max_length=100, null=True, blank=True)
cdek_tracking = models.CharField('Трек-номер СДЭК', max_length=100, null=True, blank=True)
objects = ChecklistQuerySet.as_manager()
@ -359,9 +353,9 @@ class Checklist(models.Model):
def save(self, *args, **kwargs):
if self.id:
old_obj = Checklist.objects.get(id=self.id)
old_obj = Checklist.objects.filter(id=self.id).first()
# If status was updated, update status_updated_at field
if self.status != old_obj.status:
if old_obj and self.status != old_obj.status:
self.status_updated_at = timezone.now()
# Create preview image
@ -375,11 +369,10 @@ class Checklist(models.Model):
preview.save(image_io, format='JPEG')
# Create Image model and save it
image_obj = Image(is_preview=True)
image_obj = Image(is_preview=True, checklist_id=self.id)
image_obj.image.save(name=f'{self.id}_preview.jpg',
content=ContentFile(image_io.getvalue()),
save=True)
self.images.add(image_obj)
super().save(*args, **kwargs)

View File

@ -1,8 +1,12 @@
import base64
from django.contrib.auth import authenticate
from django.core.files.base import ContentFile
from drf_extra_fields.fields import Base64ImageField
from rest_framework import serializers
from store.exceptions import CRMException, InvalidCredentialsException
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, PromoCode, Image
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image
class LoginSerializer(serializers.Serializer):
@ -44,12 +48,14 @@ class UserSerializer(serializers.ModelSerializer):
class ImageSerializer(serializers.ModelSerializer):
image = Base64ImageField()
class Meta:
model = Image
fields = ('image', )
fields = ('image',)
class OrderImageListSerializer(serializers.ListSerializer):
class ChecklistImageListSerializer(serializers.ListSerializer):
child = ImageSerializer()
def to_representation(self, data):
@ -59,44 +65,54 @@ class OrderImageListSerializer(serializers.ListSerializer):
class ChecklistSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True)
managerid = serializers.CharField(source='manager.manager_id', required=False, allow_null=True)
managerid = serializers.CharField(source='manager.manager_id', read_only=True)
link = serializers.URLField(source='product_link', required=False)
category = serializers.SlugRelatedField(slug_field='slug', queryset=Category.objects.all())
image = OrderImageListSerializer(source='main_images')
previewimage = serializers.ImageField(source='preview_image_url')
# TODO: choose from PromoCode table
# image = Base64ImageField(source='images', many=True, queryset=Image.objects.all())
previewimage = serializers.ImageField(source='preview_image_url', read_only=True)
# TODO: choose from Promocode table
promo = serializers.CharField(source='promocode', required=False)
currency = serializers.SerializerMethodField('get_yuan_rate')
curencycurency2 = serializers.DecimalField(source='price_yuan', max_digits=10, decimal_places=2)
currency3 = serializers.DecimalField(source='price_rub', max_digits=10, decimal_places=2, read_only=True)
chinadelivery = serializers.SerializerMethodField('get_delivery_price_CN')
chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', max_digits=10, decimal_places=2, read_only=True)
fullprice = serializers.DecimalField(source='full_price', max_digits=10, decimal_places=2)
realprice = serializers.DecimalField(source='real_price', max_digits=10, decimal_places=2)
comission = serializers.SerializerMethodField('get_comission')
currency3 = serializers.DecimalField(source='price_rub', read_only=True, max_digits=10, decimal_places=2)
chinadelivery = serializers.SerializerMethodField('get_delivery_price_CN', read_only=True)
chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', read_only=True, max_digits=10,
decimal_places=2)
fullprice = serializers.DecimalField(source='full_price', read_only=True, max_digits=10, decimal_places=2)
realprice = serializers.DecimalField(source='real_price', required=False, allow_null=True, max_digits=10,
decimal_places=2)
comission = serializers.SerializerMethodField('get_comission', read_only=True)
buyername = serializers.CharField(source='buyer_name')
buyerphone = serializers.CharField(source='buyer_phone')
tg = serializers.CharField(source='buyer_telegram')
buyername = serializers.CharField(source='buyer_name', required=False, allow_null=True)
buyerphone = serializers.CharField(source='buyer_phone', required=False, allow_null=True)
tg = serializers.CharField(source='buyer_telegram', required=False, allow_null=True)
receivername = serializers.CharField(source='receiver_name')
reveiverphone = serializers.CharField(source='receiver_phone')
receivername = serializers.CharField(source='receiver_name', required=False, allow_null=True)
reveiverphone = serializers.CharField(source='receiver_phone', required=False, allow_null=True)
paymenttype = serializers.SlugRelatedField(source='payment_method', slug_field='slug', queryset=PaymentMethod.objects.all())
paymentproovement = serializers.ImageField(source='payment_proof')
checkphoto = serializers.ImageField(source='cheque_photo')
trackid = serializers.CharField(source='track_number')
delivery = serializers.CharField(source='get_delivery_display')
paymenttype = serializers.SlugRelatedField(source='payment_method', slug_field='slug',
queryset=PaymentMethod.objects.all(),
required=False, allow_null=True)
paymentproovement = serializers.ImageField(source='payment_proof', required=False, allow_null=True)
checkphoto = serializers.ImageField(source='cheque_photo', required=False, allow_null=True)
trackid = serializers.CharField(source='poizon_tracking', required=False, allow_null=True)
cdek_tracking = serializers.CharField(required=False, allow_null=True)
delivery = serializers.CharField(source='get_delivery_display', required=False, allow_null=True)
startDate = serializers.DateTimeField(source='created_at')
currentDate = serializers.DateTimeField(source='status_updated_at')
startDate = serializers.DateTimeField(source='created_at', read_only=True)
currentDate = serializers.DateTimeField(source='status_updated_at', read_only=True)
@staticmethod
def get_yuan_rate(obj: Checklist):
return GlobalSettings.load().yuan_rate
@staticmethod
def get_image(obj: Checklist):
return obj.images.all()
@staticmethod
def get_delivery_price_CN(obj: Checklist):
return GlobalSettings.load().delivery_price_CN
@ -110,7 +126,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
fields = ('id', 'status', 'managerid', 'link',
'category', 'subcategory',
'brand', 'model', 'size',
'image',
# 'image',
'previewimage',
'currency', 'curencycurency2', 'currency3', 'chinadelivery', 'chinadelivery2', 'comission',
'promo',
@ -119,7 +135,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
'buyername', 'buyerphone', 'tg',
'receivername', 'reveiverphone',
'paymenttype', 'paymentproovement', 'checkphoto',
'trackid', 'delivery',
'trackid', 'cdek_tracking', 'delivery',
'startDate', 'currentDate',
)
@ -171,5 +187,5 @@ class PromocodeSerializer(serializers.ModelSerializer):
nocomission = serializers.BooleanField(source='no_comission')
class Meta:
model = PromoCode
model = Promocode
fields = ('name', 'discount', 'freedelivery', 'nocomission')

View File

@ -7,6 +7,7 @@ router = DefaultRouter()
# FIXME: renamed
router.register(r'statistics', views.StatisticsAPI, basename='statistics')
router.register(r'cdek', views.CDEKAPI, basename='cdek')
urlpatterns = [
path("login/", views.LoginAPI.as_view()),

View File

@ -1,20 +1,23 @@
import calendar
import json
from collections import OrderedDict, defaultdict
from django.conf import settings
from django.contrib.auth import login
from django.db.models import F, Count, Q, Sum
from django.db.models import F, Count, Q, Sum, Value, Subquery
from django.utils import timezone
from rest_framework import generics, permissions, mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from cdek.api import CDEKClient
from store.exceptions import CRMException
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, PromoCode
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, Promocode
from store.serializers import (UserSerializer, LoginSerializer, ChecklistSerializer, GlobalSettingsYuanRateSerializer,
CategorySerializer, GlobalSettingsPriceSerializer, PaymentMethodSerializer,
PromocodeSerializer, GlobalSettingsPickupSerializer)
from utils.permissions import ReadOnly
class UserAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView):
@ -53,8 +56,9 @@ class LoginAPI(generics.GenericAPIView):
return Response(serializer.data)
class ChecklistAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView):
class ChecklistAPI(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView):
serializer_class = ChecklistSerializer
permission_classes = [IsAuthenticated | ReadOnly]
lookup_field = 'id'
filterset_fields = ['status', ]
search_fields = ['id', 'track_id', 'buyer_phone', 'full_price']
@ -76,6 +80,9 @@ class ChecklistAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.Ge
return obj
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if 'id' in kwargs:
return self.retrieve(request, *args, **kwargs)
@ -129,8 +136,7 @@ class CategoryAPI(generics.GenericAPIView):
'prices': GlobalSettingsPriceSerializer(global_settings).data,
})
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
if not all(k in data for k in ("category", "chinarush")):
raise CRMException('category and chinarush is required')
@ -146,8 +152,7 @@ class CategoryAPI(generics.GenericAPIView):
class PricesAPI(generics.GenericAPIView):
serializer_class = GlobalSettingsPriceSerializer
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = GlobalSettings.load()
@ -160,6 +165,7 @@ class PricesAPI(generics.GenericAPIView):
class PickupAPI(generics.GenericAPIView):
serializer_class = GlobalSettingsPickupSerializer
permission_classes = [IsAuthenticated | ReadOnly]
def get_object(self):
return GlobalSettings.load()
@ -193,8 +199,7 @@ class PaymentMethodsAPI(generics.GenericAPIView):
return Response(data)
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
if 'type' not in data:
raise CRMException('type is required')
@ -212,7 +217,7 @@ class PromoCodeAPI(mixins.CreateModelMixin, generics.GenericAPIView):
lookup_field = 'name'
def get_queryset(self):
return PromoCode.objects.all()
return Promocode.objects.all()
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
@ -245,8 +250,6 @@ class StatisticsAPI(viewsets.GenericViewSet):
global_settings = GlobalSettings.load()
yuan_rate = global_settings.yuan_rate
completed_filter = Q(status=Checklist.Status.COMPLETED)
# Prepare query to collect the stats
qs = self.get_queryset() \
.annotate_price_rub() \
@ -307,51 +310,72 @@ class StatisticsAPI(viewsets.GenericViewSet):
return Response(result)
# TODO: implement stats_by_clients
@action(url_path='clients', detail=False, methods=['get'])
def stats_by_clients(self, request, *args, **kwargs):
def _create_stats(data: dict):
return {
"moreone": data.get('moreone', 0),
"moretwo": data.get('moretwo', 0),
"morethree": data.get('morethree', 0),
"morefour": data.get('morefour', 0),
"morefive": data.get('morefive', 0),
"moreten": data.get('moreten', 0),
"moretwentyfive": data.get('moretwentyfive', 0),
"morefifty": data.get('morefifty', 0),
}
options = {
"moreone": 1,
"moretwo": 2,
"morethree": 3,
"morefour": 4,
"morefive": 5,
"moreten": 10,
"moretwentyfive": 25,
"morefifty": 50,
}
def _filter_for_count(count):
return Count('buyer_phone',
filter=Q(buyer_phone__in=Checklist.objects.values('buyer_phone')
.annotate(total_orders=Count('id'))
.filter(total_orders__gt=count)
.values('buyer_phone')
))
def _create_empty_stats():
return {k: set() for k in options.keys()}
qs = self.get_queryset() \
.values('month') \
.annotate(
moreone=_filter_for_count(1),
moretwo=_filter_for_count(2),
morethree=_filter_for_count(3),
morefour=_filter_for_count(4),
morefive=_filter_for_count(5),
moreten=_filter_for_count(10),
moretwentyfive=_filter_for_count(25),
morefifty=_filter_for_count(50),
)
.filter(buyer_phone__isnull=False) \
.values('month', 'buyer_phone') \
.annotate(order_count=Count('id')) \
.filter(order_count__gt=1) \
.order_by('month')
result = {}
# Add empty stats
for i in range(1, 13):
month = calendar.month_name[i]
result[month] = _create_stats(dict())
month_name = calendar.month_name[i]
result[month_name] = _create_empty_stats()
# Add actual stats
for stat in qs:
month = calendar.month_name[stat['month']]
result[month] = _create_stats(stat)
month_name = calendar.month_name[stat['month']]
for key, size in reversed(options.items()):
if stat['order_count'] > size:
result[month_name][key].add(stat['buyer_phone'])
break
return Response(result)
class CDEKAPI(viewsets.GenericViewSet):
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
@action(url_path='orders', detail=False, methods=['get'])
def get_order_info(self, request, *args, **kwargs):
im_number = request.query_params.get('im_number')
if not im_number:
raise CRMException('im_number is required')
r = self.client.get_order_info(im_number)
return Response(r.json())
@action(url_path='orders', detail=False, methods=['post'])
def create_order(self, request, *args, **kwargs):
order_data = request.data
if not order_data:
raise CRMException('json data is required')
r = self.client.create_order(order_data)
return Response(r.json())
@action(url_path='calculator/tariff', detail=False, methods=['post'])
def calculate_tariff(self, request, *args, **kwargs):
data = request.data
if not data:
raise CRMException('json data is required')
r = self.client.calculate_tariff(data)
return Response(r.json())

View File

@ -1,8 +1,14 @@
from rest_framework import permissions
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import BasePermission, SAFE_METHODS
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
# To not perform the csrf check previously happening
return
class ReadOnly(BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS