import calendar import json from django.conf import settings from django.contrib.auth import login 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.serializers import (UserSerializer, LoginSerializer, ChecklistSerializer, GlobalSettingsYuanRateSerializer, CategorySerializer, GlobalSettingsPriceSerializer, PaymentMethodSerializer, PromocodeSerializer, GlobalSettingsPickupSerializer) from utils.permissions import ReadOnly class UserAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView): serializer_class = UserSerializer def get_queryset(self): return User.objects.all() def get(self, request, *args, **kwargs): if 'pk' in kwargs: return self.retrieve(request, *args, **kwargs) return self.list(request, *args, **kwargs) # Update some data on current user def patch(self, request, *args, **kwargs): data = json.loads(request.body) instance = self.request.user serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class LoginAPI(generics.GenericAPIView): serializer_class = LoginSerializer permission_classes = (permissions.AllowAny,) def post(self, request, *args, **kwargs): data = json.loads(request.body) serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] login(request, user) return Response(UserSerializer(user).data) class ChecklistAPI(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView): serializer_class = ChecklistSerializer permission_classes = [IsAuthenticated | ReadOnly] if not settings.DISABLE_PERMISSIONS else [permissions.AllowAny] lookup_field = 'id' filterset_fields = ['status', ] search_fields = ['id', 'track_id', 'buyer_phone', 'full_price'] 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() # 3 hours maximum in 'neworder' status -> move to drafts if obj.status == Checklist.Status.NEW: diff_hours = (timezone.now() - obj.status_updated_at).seconds / 3600 if diff_hours > 3: 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) return self.list(request, *args, **kwargs) # Update some data on current user def patch(self, request, *args, **kwargs): data = json.loads(request.body) instance = get_object_or_404(self.get_queryset(), id=self.kwargs['id']) serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class YuanRateAPI(generics.GenericAPIView): serializer_class = GlobalSettingsYuanRateSerializer def get_object(self): return GlobalSettings.load() def get(self, request, *args, **kwargs): yuan_rate = GlobalSettings.load().yuan_rate return Response(data={'currency': yuan_rate}) def patch(self, request, *args, **kwargs): data = json.loads(request.body) instance = self.get_object() serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class CategoryAPI(generics.GenericAPIView): serializer_class = CategorySerializer def get_queryset(self): return Category.objects.all() def get(self, request, *args, **kwargs): categories_qs = self.get_queryset() global_settings = GlobalSettings.load() return Response({ 'categories': CategorySerializer(categories_qs, many=True).data, 'prices': GlobalSettingsPriceSerializer(global_settings).data, }) 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') instance = get_object_or_404(self.get_queryset(), slug=data['category']) serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class PricesAPI(generics.GenericAPIView): serializer_class = GlobalSettingsPriceSerializer def patch(self, request, *args, **kwargs): data = json.loads(request.body) instance = GlobalSettings.load() serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class PickupAPI(generics.GenericAPIView): serializer_class = GlobalSettingsPickupSerializer permission_classes = [IsAuthenticated | ReadOnly] if not settings.DISABLE_PERMISSIONS else [permissions.AllowAny] 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): data = json.loads(request.body) instance = GlobalSettings.load() serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class PaymentMethodsAPI(generics.GenericAPIView): serializer_class = PaymentMethodSerializer 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 = json.loads(request.body) 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 = json.loads(request.body) 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 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: set() 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') 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: 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())