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, ChecklistFilter 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_class = ChecklistFilter 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): serializer_class = GiftSerializer permission_classes = [IsAuthenticated | ReadOnly] filterset_class = GiftFilter def get_queryset(self): if self.request.user.is_authenticated or settings.DISABLE_PERMISSIONS: return Gift.objects.all() # For anonymous users, show only available gifts return Gift.objects.filter(available_count__gt=0) 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.get_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') data = self.client.get_good_info(spu_id) return Response(data)