From 80ea0858ee049a482c6e9a03b8994788a0f3bb5e Mon Sep 17 00:00:00 2001 From: vickytechkey Date: Sun, 16 Aug 2026 15:29:44 +0530 Subject: [PATCH] feat: implement category management system and product-to-category mapping functionality --- crm/serializers.py | 15 ++++++++ crm/urls.py | 7 +++- crm/views/category_views.py | 69 +++++++++++++++++++++++++++++++++++++ customer_models/models.py | 15 ++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 crm/views/category_views.py diff --git a/crm/serializers.py b/crm/serializers.py index 5748e93..1a2837a 100644 --- a/crm/serializers.py +++ b/crm/serializers.py @@ -79,6 +79,21 @@ class WalletTransactionSerializer(serializers.ModelSerializer): model = WalletTransaction fields = ['id', 'wallet', 'amount', 'tx_type', 'status'] +class CategorySerializer(serializers.ModelSerializer): + subcategories = serializers.SerializerMethodField() + + class Meta: + model = Category + fields = ['id', 'name', 'slug', 'is_active', 'sort_order', 'parent', 'subcategories'] + + def get_subcategories(self, obj): + # We perform filtering inside Python or basic query to fetch subcategories + if obj.parent_id is None: + # Query subcategories where parent matches obj + subs = Category.objects.filter(parent=obj) + return CategorySerializer(subs, many=True).data + return [] + class CustomerProfileSerializer(serializers.ModelSerializer): name = serializers.CharField(read_only=True) email = serializers.CharField(read_only=True) diff --git a/crm/urls.py b/crm/urls.py index aeed4a5..e6e5749 100644 --- a/crm/urls.py +++ b/crm/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from crm.views import auth_views, seller_views, customer_views, product_views, wallet_views, debug_views, dashboard_views +from crm.views import auth_views, seller_views, customer_views, product_views, wallet_views, debug_views, dashboard_views, category_views urlpatterns = [ # Auth @@ -36,8 +36,13 @@ urlpatterns = [ path('products//approve/', product_views.approve_product, name='approve_product'), path('products//hold/', product_views.hold_product, name='hold_product'), path('products//reject/', product_views.reject_product, name='reject_product'), + path('products//map-category/', category_views.map_product_category, name='map_product_category'), path('products//', product_views.delete_product, name='delete_product'), + # Categories + path('categories/', category_views.list_categories, name='list_categories'), + path('categories/create/', category_views.create_category, name='create_category'), + # Wallet / Withdrawals path('wallets/withdrawals/', wallet_views.list_withdrawals, name='list_withdrawals'), path('wallets/withdrawals//approve/', wallet_views.approve_withdrawal, name='approve_withdrawal'), diff --git a/crm/views/category_views.py b/crm/views/category_views.py new file mode 100644 index 0000000..f0daba0 --- /dev/null +++ b/crm/views/category_views.py @@ -0,0 +1,69 @@ +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.response import Response +from django.utils.text import slugify +from crm.permissions import IsCRMAdmin +from customer_models.models import Category +from seller_models.models import Product +from crm.serializers import CategorySerializer +from crm.views.product_views import log_product_action + +@api_view(['GET']) +@permission_classes([IsCRMAdmin]) +def list_categories(request): + # Only return top-level categories (parent is null) + categories = Category.objects.filter(parent__isnull=True) + serializer = CategorySerializer(categories, many=True) + return Response(serializer.data) + +@api_view(['POST']) +@permission_classes([IsCRMAdmin]) +def create_category(request): + name = request.data.get('name') + parent_id = request.data.get('parent') + + if not name: + return Response({"detail": "Name is required"}, status=400) + + slug = request.data.get('slug', slugify(name)) + + # Check if category with this slug already exists + if Category.objects.filter(slug=slug).exists(): + slug = f"{slug}-{Category.objects.filter(slug__startswith=slug).count() + 1}" + + parent = None + if parent_id: + try: + parent = Category.objects.get(pk=parent_id) + except Category.DoesNotExist: + return Response({"detail": "Parent category not found"}, status=404) + + # Save category to database + category = Category.objects.create( + name=name, + slug=slug, + parent=parent, + is_active=True + ) + + serializer = CategorySerializer(category) + return Response(serializer.data, status=201) + +@api_view(['POST']) +@permission_classes([IsCRMAdmin]) +def map_product_category(request, pk): + try: + product = Product.objects.get(pk=pk) + except Product.DoesNotExist: + return Response({"detail": "Product not found"}, status=404) + + category_val = request.data.get('category') + if not category_val: + return Response({"detail": "Category value is required"}, status=400) + + # Save category field (holds name/slug of the category or subcategory) + product.category = category_val + product.save() + + log_product_action(request.user, 'map_category', product.id, {"title": product.name, "category": category_val}) + return Response({"message": f"Product mapped under category '{category_val}' successfully"}) diff --git a/customer_models/models.py b/customer_models/models.py index b027e36..105c038 100644 --- a/customer_models/models.py +++ b/customer_models/models.py @@ -1,5 +1,20 @@ from django.db import models +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(max_length=100, unique=True, blank=True) + is_active = models.BooleanField(default=True) + sort_order = models.IntegerField(default=0) + parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='subcategories', db_column='parent_id') + + class Meta: + managed = False + db_table = 'products_category' + ordering = ['sort_order', 'name'] + + def __str__(self): + return self.name + class CustomerProfile(models.Model): user_id = models.IntegerField(unique=True) phone = models.CharField(max_length=20, blank=True, null=True)