feat: add serializers, SKU generator, and views for seller central API
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
This commit is contained in:
parent
2212786d4b
commit
2326e3da1a
3 changed files with 121 additions and 12 deletions
|
|
@ -398,8 +398,16 @@ class ProductSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate_sku(self, value):
|
||||
clean_sku = str(value or '').strip().upper()
|
||||
if not clean_sku or not re.match(r'^[A-Z0-9][A-Z0-9\-]{1,99}$', clean_sku):
|
||||
clean_sku = f"TRDX-{secrets.token_hex(3).upper()}"
|
||||
clean_sku = re.sub(r'[\s_]+', '-', clean_sku)
|
||||
clean_sku = re.sub(r'[^A-Z0-9\-]', '', clean_sku)
|
||||
|
||||
request = self.context.get('request')
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
if not clean_sku or (clean_sku.startswith('TRDX-') and len(clean_sku.split('-')) <= 2):
|
||||
from .sku_generator import generate_product_sku
|
||||
category_name = self.initial_data.get('category') if hasattr(self, 'initial_data') else None
|
||||
clean_sku = generate_product_sku(user, category_name)
|
||||
|
||||
instance = getattr(self, 'instance', None)
|
||||
qs = Product.objects.filter(sku=clean_sku)
|
||||
|
|
@ -408,23 +416,22 @@ class ProductSerializer(serializers.ModelSerializer):
|
|||
|
||||
if qs.exists():
|
||||
counter = 1
|
||||
unique_sku = f"{clean_sku}-{counter}"
|
||||
unique_sku = f"{clean_sku}-{counter:02d}"
|
||||
while Product.objects.filter(sku=unique_sku).exclude(pk=instance.pk if instance else None).exists():
|
||||
counter += 1
|
||||
unique_sku = f"{clean_sku}-{counter}"
|
||||
unique_sku = f"{clean_sku}-{counter:02d}"
|
||||
clean_sku = unique_sku
|
||||
|
||||
return clean_sku
|
||||
|
||||
def validate_category(self, value):
|
||||
allowed = [
|
||||
"Apparel", "Home Decor", "Sculptures", "Kitchenware", "Jewellery", "Textiles", "Other",
|
||||
"Handicrafts & Art", "Handloom & Textiles", "Home & Living", "Food & Organic",
|
||||
"Beauty & Herbs", "Toys & Folk Art", "Other Craft", "Pottery & Ceramics",
|
||||
"Brass & Metal Craft", "Woodcraft & Carvings"
|
||||
]
|
||||
from .sku_generator import CATEGORY_CODES
|
||||
allowed = list(CATEGORY_CODES.keys()) + ["Other"]
|
||||
if value not in allowed:
|
||||
return "Other"
|
||||
for cat in allowed:
|
||||
if cat.lower() == str(value).strip().lower():
|
||||
return cat
|
||||
return value or "Handicrafts & Folk Art"
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
|
|
|
|||
99
api/sku_generator.py
Normal file
99
api/sku_generator.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('api')
|
||||
|
||||
CATEGORY_CODES = {
|
||||
'Food & Beverages': 'FNB',
|
||||
'Personal Care': 'PCR',
|
||||
'Hygiene & Reusable Care': 'HYG',
|
||||
'Clothing & Textiles': 'TEX',
|
||||
'Bags, Footwear & Leather': 'LEA',
|
||||
'Jewellery': 'JWL',
|
||||
'Home Decor & Furnishings': 'DEC',
|
||||
'Kitchen & Dining': 'KIT',
|
||||
'Eco Packaging & Disposables': 'PKG',
|
||||
'Handicrafts & Folk Art': 'ART',
|
||||
'Wooden, Bamboo & Coir Products': 'WOD',
|
||||
'Toys & Games': 'TOY',
|
||||
'Baby & Kids': 'KID',
|
||||
'Pet Products': 'PET',
|
||||
'Eco Cleaning Products': 'CLN',
|
||||
'Farm, Garden & Compost': 'GRD',
|
||||
'Stationery, Office & Tech': 'STN',
|
||||
'Engineering & Manufactured Goods': 'ENG',
|
||||
# Legacy categories mapped to official codes
|
||||
'Handicrafts & Art': 'ART',
|
||||
'Handloom & Textiles': 'TEX',
|
||||
'Home & Living': 'DEC',
|
||||
'Food & Organic': 'FNB',
|
||||
'Beauty & Herbs': 'PCR',
|
||||
'Toys & Folk Art': 'TOY',
|
||||
'Pottery & Ceramics': 'ART',
|
||||
'Brass & Metal Craft': 'ART',
|
||||
'Woodcraft & Carvings': 'WOD',
|
||||
'Apparel': 'TEX',
|
||||
'Sculptures': 'ART',
|
||||
'Kitchenware': 'KIT',
|
||||
'Textiles': 'TEX',
|
||||
}
|
||||
|
||||
def get_category_code(category_name):
|
||||
"""
|
||||
Returns the standardized 3-letter uppercase category code.
|
||||
"""
|
||||
if not category_name:
|
||||
return 'GEN'
|
||||
clean = str(category_name).strip()
|
||||
if clean in CATEGORY_CODES:
|
||||
return CATEGORY_CODES[clean]
|
||||
|
||||
lower = clean.lower()
|
||||
for cat, code in CATEGORY_CODES.items():
|
||||
if cat.lower() == lower or cat.lower() in lower or lower in cat.lower():
|
||||
return code
|
||||
|
||||
alnum = re.sub(r'[^A-Z0-9]', '', clean.upper())
|
||||
return (alnum[:3] if len(alnum) >= 3 else alnum.ljust(3, 'X'))
|
||||
|
||||
def normalize_merchant_id(user):
|
||||
"""
|
||||
Extracts and standardizes the seller's Merchant ID (TRDPX000001 format).
|
||||
"""
|
||||
if not user:
|
||||
return 'TRDPX000001'
|
||||
|
||||
profile = getattr(user, 'profile', None)
|
||||
mid = getattr(profile, 'merchant_id', None)
|
||||
if not mid:
|
||||
draft = getattr(user, 'onboarding_draft', None)
|
||||
mid = getattr(draft, 'merchant_id', None)
|
||||
|
||||
if mid and str(mid).strip():
|
||||
return str(mid).strip().upper().replace(' ', '')
|
||||
|
||||
user_id = getattr(user, 'id', 1)
|
||||
return f"TRDPX{str(user_id).zfill(6)}"
|
||||
|
||||
def generate_product_sku(user, category_name=None, product_model=None):
|
||||
"""
|
||||
Generates a unique SKU in format: TRDX-[categorycode]-[sellermid]-[productsno]
|
||||
e.g. TRDX-TEX-TRDPX000042-001
|
||||
"""
|
||||
if product_model is None:
|
||||
from .models import Product
|
||||
product_model = Product
|
||||
|
||||
cat_code = get_category_code(category_name)
|
||||
mid = normalize_merchant_id(user)
|
||||
|
||||
user_product_count = product_model.objects.filter(supplier=user).count() if user else 0
|
||||
seq = user_product_count + 1
|
||||
|
||||
candidate_sku = f"TRDX-{cat_code}-{mid}-{seq:03d}"
|
||||
while product_model.objects.filter(sku=candidate_sku).exists():
|
||||
seq += 1
|
||||
candidate_sku = f"TRDX-{cat_code}-{mid}-{seq:03d}"
|
||||
|
||||
logger.info("Generated SKU: %s for user=%s category=%s", candidate_sku, getattr(user, 'username', 'anon'), category_name)
|
||||
return candidate_sku
|
||||
|
|
@ -1337,7 +1337,10 @@ class BulkUploadView(APIView):
|
|||
if not title or not str(title).strip():
|
||||
row_errors.append("Missing Product Title")
|
||||
if not sku or not str(sku).strip():
|
||||
row_errors.append("Missing SKU code")
|
||||
from .sku_generator import generate_product_sku
|
||||
sku = generate_product_sku(user, category_val)
|
||||
else:
|
||||
sku = str(sku).strip().upper()
|
||||
if price_val <= 0:
|
||||
row_errors.append("Invalid Price (must be a positive number > 0)")
|
||||
if stock_val < 0:
|
||||
|
|
|
|||
Loading…
Reference in a new issue