feat: add sellers app, database router, serializers, views, and frontend pages
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 2m8s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 2m8s
This commit is contained in:
parent
593c108c12
commit
a4e46dc22c
7 changed files with 422 additions and 34 deletions
|
|
@ -10,9 +10,9 @@ class CRMDatabaseRouter:
|
|||
app = model._meta.app_label
|
||||
name = model._meta.model_name
|
||||
if app in ["sellers", "seller_models"]:
|
||||
if name == "product":
|
||||
if name in ["product", "bulkuploadlog"]:
|
||||
return "product_db"
|
||||
if name in ["wallet", "wallettransaction", "order", "returnrequest"]:
|
||||
if name in ["wallet", "wallettransaction", "order", "sellerorder", "returnrequest"]:
|
||||
return "customer_db"
|
||||
return "seller_db"
|
||||
if app in ["customers", "customer_models"]:
|
||||
|
|
@ -23,9 +23,9 @@ class CRMDatabaseRouter:
|
|||
app = model._meta.app_label
|
||||
name = model._meta.model_name
|
||||
if app in ["sellers", "seller_models"]:
|
||||
if name == "product":
|
||||
if name in ["product", "bulkuploadlog"]:
|
||||
return "product_db"
|
||||
if name in ["wallet", "wallettransaction", "order", "returnrequest"]:
|
||||
if name in ["wallet", "wallettransaction", "order", "sellerorder", "returnrequest"]:
|
||||
return "customer_db"
|
||||
return "seller_db"
|
||||
if app in ["customers", "customer_models"]:
|
||||
|
|
|
|||
|
|
@ -106,7 +106,18 @@ class Product(models.Model):
|
|||
compare_at_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
is_gi_tagged = models.BooleanField(default=False)
|
||||
gi_tag_name = models.CharField(max_length=200, blank=True, null=True)
|
||||
is_odop = models.BooleanField(default=False)
|
||||
odop_district = models.CharField(max_length=200, blank=True, null=True)
|
||||
is_sustainable = models.BooleanField(default=False)
|
||||
sustainable_type = models.CharField(max_length=200, blank=True, null=True)
|
||||
geographical_origin = models.CharField(max_length=255, blank=True, null=True)
|
||||
statutory_certifications = models.TextField(blank=True, null=True)
|
||||
provenance_story = models.TextField(blank=True, null=True)
|
||||
coordinates = models.CharField(max_length=100, blank=True, null=True)
|
||||
track_inventory = models.BooleanField(default=True)
|
||||
shipping_profile = models.CharField(max_length=100, default='Standard Fragile')
|
||||
processing_days = models.IntegerField(default=3)
|
||||
package_weight = models.FloatField(default=1.0)
|
||||
upload_source = models.CharField(max_length=50, default='individual')
|
||||
rejection_reason = models.TextField(blank=True, null=True)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class SupplierProfileListSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_products_count(self, obj):
|
||||
try:
|
||||
return Product.objects.filter(supplier_id=obj.id).count()
|
||||
return Product.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).count()
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
|
@ -165,13 +165,15 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_metrics(self, obj):
|
||||
try:
|
||||
supplier_q = Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)
|
||||
|
||||
# 1. Total Sales from delivered or all accepted orders
|
||||
orders_qs = SellerOrder.objects.filter(supplier_id=obj.id)
|
||||
orders_qs = SellerOrder.objects.filter(supplier_q)
|
||||
sales_agg = orders_qs.exclude(status__in=['Cancelled', 'Rejected']).aggregate(total=Sum('total'))
|
||||
total_sales = float(sales_agg['total'] or 0.0)
|
||||
|
||||
# 2. Total items in stock
|
||||
products_qs = Product.objects.filter(supplier_id=obj.id)
|
||||
products_qs = Product.objects.filter(supplier_q)
|
||||
stock_agg = products_qs.aggregate(total_stock=Sum('stock'))
|
||||
total_stock = int(stock_agg['total_stock'] or 0)
|
||||
|
||||
|
|
@ -183,7 +185,7 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
|
|||
).count()
|
||||
|
||||
# 4. Total amount we need to pay to seller (outstanding wallet balance)
|
||||
wallet = Wallet.objects.filter(supplier_id=obj.id).first()
|
||||
wallet = Wallet.objects.filter(supplier_q).first()
|
||||
payable_amt = float(wallet.outstanding) if wallet else 0.0
|
||||
|
||||
return {
|
||||
|
|
@ -214,22 +216,21 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_products(self, obj):
|
||||
try:
|
||||
items = Product.objects.filter(supplier_id=obj.id).order_by("-id")[:100]
|
||||
items = Product.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:100]
|
||||
return ProductSerializer(items, many=True).data
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_bulk_upload_logs(self, obj):
|
||||
try:
|
||||
# Query by user_id or supplier_id
|
||||
logs = BulkUploadLog.objects.filter(supplier_id=obj.user_id).order_by("-id")[:20]
|
||||
logs = BulkUploadLog.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:20]
|
||||
return BulkUploadLogSerializer(logs, many=True).data
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_wallet(self, obj):
|
||||
try:
|
||||
wallet = Wallet.objects.filter(supplier_id=obj.id).first()
|
||||
wallet = Wallet.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).first()
|
||||
if wallet:
|
||||
return WalletSerializer(wallet).data
|
||||
except Exception:
|
||||
|
|
@ -238,7 +239,7 @@ class SupplierProfileDetailSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_orders(self, obj):
|
||||
try:
|
||||
orders = SellerOrder.objects.filter(supplier_id=obj.id).order_by("-id")[:50]
|
||||
orders = SellerOrder.objects.filter(Q(supplier_id=obj.id) | Q(supplier_id=obj.user_id)).order_by("-id")[:50]
|
||||
return SellerOrderSerializer(orders, many=True).data
|
||||
except Exception:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -295,9 +295,15 @@ class SellerProductApproveView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk, product_id):
|
||||
try:
|
||||
product = Product.objects.get(pk=product_id, supplier_id=pk)
|
||||
except Product.DoesNotExist:
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product.status = "approved"
|
||||
|
|
@ -316,9 +322,15 @@ class SellerProductRejectView(APIView):
|
|||
|
||||
def post(self, request, pk, product_id):
|
||||
reason = request.data.get("reason", "Product does not comply with catalogue quality or listing policies.")
|
||||
try:
|
||||
product = Product.objects.get(pk=product_id, supplier_id=pk)
|
||||
except Product.DoesNotExist:
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product.status = "rejected"
|
||||
|
|
@ -337,16 +349,28 @@ class SellerProductDetailView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, product_id):
|
||||
try:
|
||||
product = Product.objects.get(pk=product_id, supplier_id=pk)
|
||||
except Product.DoesNotExist:
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(ProductSerializer(product).data)
|
||||
|
||||
def patch(self, request, pk, product_id):
|
||||
try:
|
||||
product = Product.objects.get(pk=product_id, supplier_id=pk)
|
||||
except Product.DoesNotExist:
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
updatable_fields = [
|
||||
|
|
@ -358,6 +382,15 @@ class SellerProductDetailView(APIView):
|
|||
"sku",
|
||||
"description",
|
||||
"is_gi_tagged",
|
||||
"gi_tag_name",
|
||||
"is_odop",
|
||||
"odop_district",
|
||||
"is_sustainable",
|
||||
"sustainable_type",
|
||||
"geographical_origin",
|
||||
"statutory_certifications",
|
||||
"provenance_story",
|
||||
"coordinates",
|
||||
"is_active",
|
||||
"status",
|
||||
"rejection_reason",
|
||||
|
|
@ -381,9 +414,15 @@ class SellerProductToggleView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk, product_id):
|
||||
try:
|
||||
product = Product.objects.get(pk=product_id, supplier_id=pk)
|
||||
except Product.DoesNotExist:
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if "is_active" in request.data:
|
||||
|
|
@ -403,8 +442,13 @@ class SellerOrderTrackView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, order_id):
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
supplier_id_q = models.Q(supplier_id=pk)
|
||||
if seller:
|
||||
supplier_id_q = models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id)
|
||||
|
||||
try:
|
||||
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id), supplier_id=pk).first()
|
||||
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id)).filter(supplier_id_q).first()
|
||||
if not order:
|
||||
# Also fallback to general order search
|
||||
order = SellerOrder.objects.filter(models.Q(order_id=order_id) | models.Q(id=order_id)).first()
|
||||
|
|
|
|||
307
backend/test_crm_seller_products.py
Normal file
307
backend/test_crm_seller_products.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock celery before importing settings
|
||||
sys.modules['celery'] = MagicMock()
|
||||
sys.modules['celery.schedules'] = MagicMock()
|
||||
sys.modules['django_celery_beat'] = MagicMock()
|
||||
sys.modules['drf_spectacular'] = MagicMock()
|
||||
sys.modules['drf_spectacular.extensions'] = MagicMock()
|
||||
|
||||
sys.path.insert(0, 'D:/Tradhox_workspace/Django-CRM/backend')
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'crm.settings'
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
# Override settings for standalone unit test
|
||||
settings.DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
'seller_db': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
'product_db': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
'customer_db': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
}
|
||||
settings.REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [],
|
||||
'DEFAULT_PERMISSION_CLASSES': [],
|
||||
}
|
||||
settings.DATABASE_ROUTERS = ['crm.db_router.CRMDatabaseRouter']
|
||||
settings.AUTH_USER_MODEL = 'auth.User'
|
||||
settings.INSTALLED_APPS = [
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
'rest_framework',
|
||||
'sellers',
|
||||
]
|
||||
|
||||
django.setup()
|
||||
|
||||
from sellers.models import SupplierProfile, Product, BulkUploadLog, SellerOrder, Wallet
|
||||
from sellers.serializers import (
|
||||
SupplierProfileListSerializer,
|
||||
SupplierProfileDetailSerializer,
|
||||
ProductSerializer
|
||||
)
|
||||
from crm.db_router import CRMDatabaseRouter
|
||||
|
||||
# 1. Test Router mappings
|
||||
router = CRMDatabaseRouter()
|
||||
assert router.db_for_read(Product) == 'product_db', f"Product should route to product_db, got {router.db_for_read(Product)}"
|
||||
assert router.db_for_read(BulkUploadLog) == 'product_db', f"BulkUploadLog should route to product_db, got {router.db_for_read(BulkUploadLog)}"
|
||||
assert router.db_for_read(SellerOrder) == 'customer_db', f"SellerOrder should route to customer_db, got {router.db_for_read(SellerOrder)}"
|
||||
assert router.db_for_read(Wallet) == 'customer_db', f"Wallet should route to customer_db, got {router.db_for_read(Wallet)}"
|
||||
assert router.db_for_read(SupplierProfile) == 'seller_db', f"SupplierProfile should route to seller_db, got {router.db_for_read(SupplierProfile)}"
|
||||
print("CRMDatabaseRouter checks passed!")
|
||||
|
||||
# 2. Create tables in test DBs
|
||||
from django.db import connections
|
||||
|
||||
def create_table_product():
|
||||
with connections['product_db'].cursor() as c:
|
||||
c.execute("""
|
||||
CREATE TABLE api_product (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
supplier_id INTEGER,
|
||||
title VARCHAR(255),
|
||||
category VARCHAR(100),
|
||||
price DECIMAL(10, 2),
|
||||
stock INTEGER,
|
||||
sku VARCHAR(100),
|
||||
image TEXT,
|
||||
is_active BOOLEAN,
|
||||
status VARCHAR(50),
|
||||
compare_at_price DECIMAL(10, 2),
|
||||
description TEXT,
|
||||
is_gi_tagged BOOLEAN,
|
||||
gi_tag_name VARCHAR(200),
|
||||
is_odop BOOLEAN,
|
||||
odop_district VARCHAR(200),
|
||||
is_sustainable BOOLEAN,
|
||||
sustainable_type VARCHAR(200),
|
||||
geographical_origin VARCHAR(255),
|
||||
statutory_certifications TEXT,
|
||||
provenance_story TEXT,
|
||||
coordinates VARCHAR(100),
|
||||
track_inventory BOOLEAN,
|
||||
shipping_profile VARCHAR(100),
|
||||
processing_days INTEGER,
|
||||
package_weight REAL,
|
||||
upload_source VARCHAR(50),
|
||||
rejection_reason TEXT,
|
||||
additional_images TEXT,
|
||||
video_url VARCHAR(200),
|
||||
view_360_url VARCHAR(200)
|
||||
)
|
||||
""")
|
||||
|
||||
def create_table_seller():
|
||||
with connections['seller_db'].cursor() as c:
|
||||
c.execute("""
|
||||
CREATE TABLE api_supplierprofile (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
phone_verified BOOLEAN,
|
||||
email_verified BOOLEAN,
|
||||
gstin VARCHAR(15),
|
||||
is_gstin_verified BOOLEAN,
|
||||
pan_number VARCHAR(20),
|
||||
is_pan_verified BOOLEAN,
|
||||
verified_pan_name VARCHAR(255),
|
||||
verified_gst_name VARCHAR(255),
|
||||
is_verified BOOLEAN,
|
||||
business_type VARCHAR(50),
|
||||
store_slug VARCHAR(255),
|
||||
aadhar_file VARCHAR(255),
|
||||
pan_file VARCHAR(255),
|
||||
aadhar_s3_key VARCHAR(255),
|
||||
pan_s3_key VARCHAR(255),
|
||||
store_name VARCHAR(255),
|
||||
store_logo TEXT,
|
||||
logo_s3_key VARCHAR(255),
|
||||
business_bio TEXT,
|
||||
street VARCHAR(255),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
pincode VARCHAR(20),
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
status VARCHAR(50),
|
||||
submitted_at DATETIME,
|
||||
approved_at DATETIME,
|
||||
approved_by VARCHAR(150),
|
||||
rejected_reason TEXT,
|
||||
is_profile_complete BOOLEAN,
|
||||
pickup_code VARCHAR(100),
|
||||
account_holder_name VARCHAR(255),
|
||||
account_number VARCHAR(50),
|
||||
ifsc_code VARCHAR(20),
|
||||
bank_name VARCHAR(255),
|
||||
is_bank_verified BOOLEAN,
|
||||
tax_mode VARCHAR(20)
|
||||
)
|
||||
""")
|
||||
|
||||
def create_table_customer():
|
||||
with connections['customer_db'].cursor() as c:
|
||||
c.execute("""
|
||||
CREATE TABLE api_order (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id VARCHAR(100),
|
||||
customer VARCHAR(255),
|
||||
item VARCHAR(255),
|
||||
quantity INTEGER,
|
||||
total DECIMAL(12, 2),
|
||||
status VARCHAR(50),
|
||||
carrier VARCHAR(100),
|
||||
tracking VARCHAR(100),
|
||||
date DATE,
|
||||
supplier_id INTEGER,
|
||||
payment_mode VARCHAR(20),
|
||||
eta VARCHAR(50),
|
||||
expected_delivery_date VARCHAR(100),
|
||||
pickup_id VARCHAR(100),
|
||||
pickup_payload TEXT
|
||||
)
|
||||
""")
|
||||
c.execute("""
|
||||
CREATE TABLE api_wallet (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
supplier_id INTEGER UNIQUE,
|
||||
outstanding DECIMAL(12, 2),
|
||||
withdrawn DECIMAL(12, 2)
|
||||
)
|
||||
""")
|
||||
|
||||
create_table_product()
|
||||
create_table_seller()
|
||||
create_table_customer()
|
||||
|
||||
# Create SupplierProfile with id=46, user_id=28
|
||||
seller = SupplierProfile.objects.create(
|
||||
id=46,
|
||||
user_id=28,
|
||||
store_name="vignesh store",
|
||||
phone="9600494039",
|
||||
status="approved"
|
||||
)
|
||||
|
||||
# Create Product with supplier_id=28 (which is seller.user_id)
|
||||
product = Product.objects.create(
|
||||
id=20,
|
||||
supplier_id=28,
|
||||
title="Pure Kanchipuram Mulberry Silk Saree - Crimson Maroon & Gold Zari",
|
||||
category="Clothing & Textiles",
|
||||
price=14500.00,
|
||||
stock=8,
|
||||
sku="TRDX-KSS-960049",
|
||||
is_gi_tagged=True,
|
||||
gi_tag_name="Kancheepuram Silk (GI Application #26)",
|
||||
is_odop=True,
|
||||
odop_district="Kanchipuram, Tamil Nadu",
|
||||
is_sustainable=True,
|
||||
sustainable_type="Handloom Organic Mulberry Silk & Eco-dyes",
|
||||
geographical_origin="Kanchipuram, Tamil Nadu, India",
|
||||
statutory_certifications="GI Tag #26 Registered, Silk Mark Certified, Handloom Mark India",
|
||||
status="pending",
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Test SupplierProfileListSerializer
|
||||
list_data = SupplierProfileListSerializer(seller).data
|
||||
print(f"SupplierProfileListSerializer products_count: {list_data['products_count']}")
|
||||
assert list_data['products_count'] == 1, f"Expected products_count 1, got {list_data['products_count']}"
|
||||
|
||||
# Test SupplierProfileDetailSerializer
|
||||
detail_data = SupplierProfileDetailSerializer(seller).data
|
||||
print(f"Detail metrics: {detail_data['metrics']}")
|
||||
assert detail_data['metrics']['total_products'] == 1, f"Expected 1 total_products, got {detail_data['metrics']['total_products']}"
|
||||
assert detail_data['metrics']['total_items_in_stock'] == 8, f"Expected 8 items in stock, got {detail_data['metrics']['total_items_in_stock']}"
|
||||
assert len(detail_data['products']) == 1, f"Expected 1 product in products list, got {len(detail_data['products'])}"
|
||||
|
||||
prod_data = detail_data['products'][0]
|
||||
print(f"Product in detail: ID={prod_data['id']}, title='{prod_data['title']}', is_gi_tagged={prod_data['is_gi_tagged']}, is_odop={prod_data['is_odop']}, is_sustainable={prod_data['is_sustainable']}")
|
||||
assert prod_data['id'] == 20
|
||||
assert prod_data['is_gi_tagged'] is True
|
||||
assert prod_data['is_odop'] is True
|
||||
assert prod_data['is_sustainable'] is True
|
||||
|
||||
print("ALL SERIALIZER AND MODEL TESTS PASSED SUCCESSFULLY!")
|
||||
|
||||
# 3. Test Views logic (Approve, Reject, Toggle, Edit Content)
|
||||
from sellers.views import (
|
||||
SellerProductApproveView,
|
||||
SellerProductRejectView,
|
||||
SellerProductToggleView,
|
||||
SellerProductDetailView
|
||||
)
|
||||
from django.core.management import call_command
|
||||
call_command('migrate', database='default', run_syncdb=True, verbosity=0)
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
factory = APIRequestFactory()
|
||||
admin_user = User.objects.create_user(username='admin', email='admin@tradhox.com')
|
||||
|
||||
# Test Approve
|
||||
req = factory.post(f"/api/sellers/46/products/20/approve/")
|
||||
force_authenticate(req, user=admin_user)
|
||||
resp = SellerProductApproveView.as_view()(req, pk=46, product_id=20)
|
||||
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.data}"
|
||||
p = Product.objects.using('product_db').get(pk=20)
|
||||
assert p.status == 'approved'
|
||||
assert p.is_active is True
|
||||
print("SellerProductApproveView passed!")
|
||||
|
||||
# Test Reject
|
||||
req = factory.post(f"/api/sellers/46/products/20/reject/", {"reason": "Quality check required"}, format="json")
|
||||
force_authenticate(req, user=admin_user)
|
||||
resp = SellerProductRejectView.as_view()(req, pk=46, product_id=20)
|
||||
assert resp.status_code == 200
|
||||
p = Product.objects.using('product_db').get(pk=20)
|
||||
assert p.status == 'rejected'
|
||||
assert p.is_active is False
|
||||
assert p.rejection_reason == "Quality check required"
|
||||
print("SellerProductRejectView passed!")
|
||||
|
||||
# Test Toggle
|
||||
req = factory.post(f"/api/sellers/46/products/20/toggle/", {"is_active": True, "status": "approved"}, format="json")
|
||||
force_authenticate(req, user=admin_user)
|
||||
resp = SellerProductToggleView.as_view()(req, pk=46, product_id=20)
|
||||
assert resp.status_code == 200
|
||||
p = Product.objects.using('product_db').get(pk=20)
|
||||
assert p.is_active is True
|
||||
assert p.status == 'approved'
|
||||
print("SellerProductToggleView passed!")
|
||||
|
||||
# Test Detail PATCH
|
||||
req = factory.patch(f"/api/sellers/46/products/20/", {
|
||||
"price": 15200.00,
|
||||
"stock": 12,
|
||||
"is_odop": True,
|
||||
"odop_district": "Kanchipuram District",
|
||||
"geographical_origin": "Kanchipuram, Tamil Nadu"
|
||||
}, format="json")
|
||||
force_authenticate(req, user=admin_user)
|
||||
resp = SellerProductDetailView.as_view()(req, pk=46, product_id=20)
|
||||
assert resp.status_code == 200
|
||||
p = Product.objects.using('product_db').get(pk=20)
|
||||
assert float(p.price) == 15200.00
|
||||
assert p.stock == 12
|
||||
assert p.odop_district == "Kanchipuram District"
|
||||
print("SellerProductDetailView PATCH passed!")
|
||||
|
||||
print("ALL VIEWS AND API ENDPOINT TESTS PASSED SUCCESSFULLY!")
|
||||
|
|
@ -165,7 +165,7 @@ export const actions = {
|
|||
const payload = {};
|
||||
for (const [key, value] of data.entries()) {
|
||||
if (key !== 'product_id' && key !== 'action') {
|
||||
if (key === 'is_gi_tagged' || key === 'is_active') {
|
||||
if (key === 'is_gi_tagged' || key === 'is_active' || key === 'is_odop' || key === 'is_sustainable') {
|
||||
payload[key] = value === 'true';
|
||||
} else if (key === 'price' || key === 'compare_at_price' || key === 'stock' || key === 'package_weight') {
|
||||
if (value !== '') payload[key] = Number(value);
|
||||
|
|
|
|||
|
|
@ -624,11 +624,17 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;">
|
||||
<div style="font-weight:600;font-size:13px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||
{p.title}
|
||||
{#if p.is_gi_tagged}
|
||||
<span style="font-size:10px;background:#FDF2F4;color:#671B26;padding:1px 5px;border-radius:4px;font-weight:700;">GI TAG</span>
|
||||
{/if}
|
||||
{#if p.is_odop}
|
||||
<span style="font-size:10px;background:#F0FDF4;color:#166534;padding:1px 5px;border-radius:4px;font-weight:700;">ODOP</span>
|
||||
{/if}
|
||||
{#if p.is_sustainable}
|
||||
<span style="font-size:10px;background:#EFF6FF;color:#1E40AF;padding:1px 5px;border-radius:4px;font-weight:700;">SUSTAINABLE</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="v2-muted" style="font-size:11px;">SKU: {p.sku || '—'} · ID: #{p.id}</div>
|
||||
{#if p.rejection_reason}
|
||||
|
|
@ -917,14 +923,33 @@
|
|||
<textarea id="description" name="description" bind:value={editingProduct.description} rows="4" class="v2-input" style="width:100%;font-size:13px;padding:8px;"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;display:block;margin-bottom:4px;" for="geographical_origin">Geographical Origin</label>
|
||||
<input type="text" id="geographical_origin" name="geographical_origin" bind:value={editingProduct.geographical_origin} class="v2-input" style="width:100%;font-size:13px;padding:8px;" placeholder="e.g. Kanchipuram, Tamil Nadu" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="v2-sub" style="font-size:12px;display:block;margin-bottom:4px;" for="statutory_certifications">Statutory Certifications</label>
|
||||
<input type="text" id="statutory_certifications" name="statutory_certifications" bind:value={editingProduct.statutory_certifications} class="v2-input" style="width:100%;font-size:13px;padding:8px;" placeholder="e.g. Silk Mark, Handloom Mark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_gi_tagged" value="true" checked={editingProduct.is_gi_tagged} />
|
||||
<span>Certified Geographical Indication (GI Tag)</span>
|
||||
<span>Certified GI Tag</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_odop" value="true" checked={editingProduct.is_odop} />
|
||||
<span>ODOP Product</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_sustainable" value="true" checked={editingProduct.is_sustainable} />
|
||||
<span>Sustainable Craft</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_active" value="true" checked={editingProduct.is_active} />
|
||||
<span>Visible in Website Storefront</span>
|
||||
<span>Visible in Storefront</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue