385 lines
13 KiB
Python
385 lines
13 KiB
Python
import calendar
|
|
from datetime import timedelta
|
|
|
|
from django.conf import settings
|
|
from django.db.models import F, Count, Sum, OuterRef, Subquery
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import generics, permissions, mixins, status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.filters import SearchFilter
|
|
from rest_framework.generics import get_object_or_404
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from external_api.cdek import CDEKClient
|
|
from external_api.poizon import PoizonClient
|
|
from store.exceptions import CRMException
|
|
from store.filters import GiftFilter
|
|
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Gift
|
|
from store.serializers import (ChecklistSerializer, CategorySerializer, CategoryFullSerializer,
|
|
PaymentMethodSerializer, GlobalSettingsSerializer,
|
|
PromocodeSerializer, AnonymousUserChecklistSerializer, GiftSerializer)
|
|
from utils.permissions import ReadOnly
|
|
|
|
|
|
class DisablePermissionsMixin(generics.GenericAPIView):
|
|
def get_permissions(self):
|
|
if settings.DISABLE_PERMISSIONS:
|
|
return [permissions.AllowAny()]
|
|
|
|
return super().get_permissions()
|
|
|
|
|
|
class ChecklistAPI(mixins.ListModelMixin,
|
|
mixins.CreateModelMixin,
|
|
mixins.RetrieveModelMixin,
|
|
mixins.UpdateModelMixin,
|
|
mixins.DestroyModelMixin,
|
|
DisablePermissionsMixin):
|
|
serializer_class = ChecklistSerializer
|
|
lookup_field = 'id'
|
|
filterset_fields = ['status', ]
|
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
|
search_fields = ['id', 'poizon_tracking', 'buyer_phone']
|
|
# TODO: search by full_price
|
|
|
|
def get_serializer_class(self):
|
|
if self.request.user.is_authenticated or settings.DISABLE_PERMISSIONS:
|
|
return ChecklistSerializer
|
|
|
|
# Anonymous users can edit only a certain set of fields
|
|
return AnonymousUserChecklistSerializer
|
|
|
|
def get_permissions(self):
|
|
if self.request.method in ('GET', 'PATCH'):
|
|
return [permissions.AllowAny()]
|
|
|
|
return super().get_permissions()
|
|
|
|
def get_queryset(self):
|
|
return Checklist.objects.all().with_base_related() \
|
|
.annotate_price_rub().annotate_commission_rub() \
|
|
.default_ordering()
|
|
|
|
def get_object(self):
|
|
obj: Checklist = super().get_object()
|
|
|
|
# N time maximum in 'neworder' status -> move to drafts
|
|
if obj.status == Checklist.Status.NEW and obj.buy_time_remaining is not None:
|
|
if obj.buy_time_remaining <= timedelta():
|
|
obj.status = Checklist.Status.DRAFT
|
|
obj.save()
|
|
|
|
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)
|
|
|
|
if not request.user.is_authenticated and not settings.DISABLE_PERMISSIONS:
|
|
# Anonymous users can't list checklists
|
|
return Response([])
|
|
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def perform_update(self, serializer):
|
|
serializer.save()
|
|
|
|
def patch(self, request, *args, **kwargs):
|
|
return self.partial_update(request, *args, **kwargs)
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
return self.destroy(request, *args, **kwargs)
|
|
|
|
|
|
class CategoryAPI(mixins.ListModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView):
|
|
serializer_class = CategorySerializer
|
|
lookup_field = 'id'
|
|
|
|
def get_queryset(self):
|
|
return Category.objects.all()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
categories_qs = Category.objects.root_nodes()
|
|
return Response(CategoryFullSerializer(categories_qs, many=True).data)
|
|
|
|
def patch(self, request, *args, **kwargs):
|
|
return self.partial_update(request, *args, **kwargs)
|
|
|
|
|
|
class GlobalSettingsAPI(generics.GenericAPIView):
|
|
serializer_class = GlobalSettingsSerializer
|
|
permission_classes = [IsAuthenticated | ReadOnly]
|
|
|
|
def get_object(self):
|
|
return GlobalSettings.load()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
return Response(self.get_serializer(instance).data)
|
|
|
|
def patch(self, request, *args, **kwargs):
|
|
instance = GlobalSettings.load()
|
|
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
serializer.save()
|
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
class PaymentMethodsAPI(generics.GenericAPIView):
|
|
serializer_class = PaymentMethodSerializer
|
|
permission_classes = [IsAuthenticated | ReadOnly]
|
|
|
|
def get_queryset(self):
|
|
return PaymentMethod.objects.all()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
qs = self.get_queryset()
|
|
data = {}
|
|
for obj in qs:
|
|
data[obj.slug] = self.get_serializer(obj).data
|
|
|
|
return Response(data)
|
|
|
|
def patch(self, request, *args, **kwargs):
|
|
data = request.data
|
|
if 'type' not in data:
|
|
raise CRMException('type is required')
|
|
|
|
instance = get_object_or_404(self.get_queryset(), slug=data['type'])
|
|
serializer = self.get_serializer(instance, data=data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
serializer.save()
|
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
class PromoCodeAPI(mixins.CreateModelMixin, generics.GenericAPIView):
|
|
serializer_class = PromocodeSerializer
|
|
lookup_field = 'name'
|
|
|
|
def get_queryset(self):
|
|
return Promocode.objects.all()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
qs = self.get_queryset()
|
|
return Response(
|
|
{'promo': self.get_serializer(qs, many=True).data}
|
|
)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.create(request, *args, **kwargs)
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
data = request.data
|
|
if 'name' not in data:
|
|
raise CRMException('name is required')
|
|
|
|
instance: Promocode = get_object_or_404(self.get_queryset(), name=data['name'])
|
|
instance.is_active = False
|
|
instance.save()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class GiftAPI(viewsets.ModelViewSet):
|
|
queryset = Gift.objects.all()
|
|
serializer_class = GiftSerializer
|
|
permission_classes = [IsAuthenticated | ReadOnly]
|
|
filterset_class = GiftFilter
|
|
|
|
|
|
class StatisticsAPI(viewsets.GenericViewSet):
|
|
def get_queryset(self):
|
|
return Checklist.objects.all() \
|
|
.filter(status=Checklist.Status.COMPLETED) \
|
|
.annotate(month=F('status_updated_at__month'))
|
|
|
|
@action(url_path='orders', detail=False, methods=['get'])
|
|
def stats_by_orders(self, request, *args, **kwargs):
|
|
global_settings = GlobalSettings.load()
|
|
yuan_rate = global_settings.yuan_rate
|
|
|
|
# Prepare query to collect the stats
|
|
qs = self.get_queryset() \
|
|
.annotate_price_rub() \
|
|
.annotate_commission_rub() \
|
|
.values('month') \
|
|
.annotate(total=Count('id'),
|
|
total_completed=Count('id'),
|
|
total_bought_yuan=Sum('real_price', default=0),
|
|
total_bought_rub=F('total_bought_yuan') * yuan_rate,
|
|
total_commission_rub=Sum('_commission_rub', default=0)
|
|
)
|
|
|
|
def _create_stats(data: dict):
|
|
return {
|
|
"CountOrders": data.get('total', 0),
|
|
"CountComplete": data.get('total_completed', 0),
|
|
"SumCommission": data.get('total_commission_rub', 0),
|
|
"SumOrders1": data.get('total_bought_yuan', 0),
|
|
"SumOrders2": data.get('total_bought_rub', 0),
|
|
}
|
|
|
|
result = {}
|
|
# Add empty stats
|
|
for i in range(1, 13):
|
|
month_name = calendar.month_name[i]
|
|
result[month_name] = _create_stats(dict())
|
|
|
|
# Add actual stats
|
|
for stat in qs:
|
|
month_name = calendar.month_name[stat['month']]
|
|
result[month_name] = _create_stats(stat)
|
|
|
|
return Response(result)
|
|
|
|
@action(url_path='categories', detail=False, methods=['get'])
|
|
def stats_by_categories(self, request, *args, **kwargs):
|
|
all_categories = Category.objects.values_list('slug', flat=True)
|
|
categories_dict = {c: 0 for c in all_categories}
|
|
|
|
qs = self.get_queryset() \
|
|
.select_related('category') \
|
|
.values('month', 'category__slug') \
|
|
.annotate(total_orders=Count('id'))
|
|
|
|
result = {}
|
|
# Add empty stats
|
|
for i in range(1, 13):
|
|
month = calendar.month_name[i]
|
|
result[month] = categories_dict.copy()
|
|
|
|
# Add actual stats
|
|
for stat in qs:
|
|
month = calendar.month_name[stat['month']]
|
|
category_slug = stat['category__slug']
|
|
total_orders = stat['total_orders']
|
|
|
|
result[month][category_slug] = total_orders
|
|
|
|
return Response(result)
|
|
|
|
@action(url_path='clients', detail=False, methods=['get'])
|
|
def stats_by_clients(self, request, *args, **kwargs):
|
|
options = {
|
|
"moreone": 1,
|
|
"moretwo": 2,
|
|
"morethree": 3,
|
|
"morefour": 4,
|
|
"morefive": 5,
|
|
"moreten": 10,
|
|
"moretwentyfive": 25,
|
|
"morefifty": 50,
|
|
}
|
|
|
|
def _create_empty_stats():
|
|
return {k: [] for k in options.keys()}
|
|
|
|
qs = self.get_queryset() \
|
|
.filter(buyer_phone__isnull=False) \
|
|
.values('month', 'buyer_phone') \
|
|
.annotate(order_count=Count('id')) \
|
|
.filter(order_count__gt=1) \
|
|
.order_by('month')
|
|
|
|
# Temporary hack: collect the most recent info about client
|
|
# mapping buyer_phone -> buyer info (name, telegram)
|
|
client_mapping = {}
|
|
|
|
recent_created_at = Checklist.objects.all() \
|
|
.filter(buyer_phone=OuterRef('buyer_phone')) \
|
|
.order_by('-created_at') \
|
|
.values('created_at')[:1]
|
|
|
|
client_qs = Checklist.objects.filter(
|
|
created_at=Subquery(recent_created_at),
|
|
buyer_phone=F('buyer_phone')
|
|
).distinct()
|
|
|
|
for checklist in client_qs:
|
|
client_mapping[checklist.buyer_phone] = {
|
|
'phone': checklist.buyer_phone,
|
|
'name': checklist.buyer_name,
|
|
'telegram': checklist.buyer_telegram}
|
|
|
|
result = {}
|
|
# Add empty stats
|
|
for i in range(1, 13):
|
|
month_name = calendar.month_name[i]
|
|
result[month_name] = _create_empty_stats()
|
|
|
|
# Add actual stats
|
|
for stat in qs:
|
|
month_name = calendar.month_name[stat['month']]
|
|
|
|
for key, size in reversed(options.items()):
|
|
if stat['order_count'] > size:
|
|
client_info = client_mapping[stat['buyer_phone']]
|
|
result[month_name][key].append(client_info)
|
|
break
|
|
|
|
return Response(result)
|
|
|
|
|
|
class CDEKAPI(viewsets.GenericViewSet):
|
|
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
@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())
|
|
|
|
@get_order_info.mapping.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())
|
|
|
|
@get_order_info.mapping.patch
|
|
def edit_order(self, request, *args, **kwargs):
|
|
order_data = request.data
|
|
if not order_data:
|
|
raise CRMException('json data is required')
|
|
|
|
r = self.client.edit_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())
|
|
|
|
|
|
class PoizonAPI(viewsets.GenericViewSet):
|
|
client = PoizonClient(settings.POIZON_TOKEN)
|
|
|
|
@action(url_path='good', detail=False, methods=['post'])
|
|
def get_good_info(self, request, *args, **kwargs):
|
|
spu_id = None
|
|
if 'url' in request.data:
|
|
spu_id = self.client.get_spu_id(request.data.get('url'))
|
|
if 'spuId' in request.data:
|
|
spu_id = request.data.get('spuId')
|
|
|
|
if spu_id is None:
|
|
raise CRMException('url or spuId is required')
|
|
|
|
r = self.client.get_good_info(spu_id)
|
|
return Response(r.json())
|