From 2326e3da1a4a183bf91ca3d0f2fa364fe72ffadd Mon Sep 17 00:00:00 2001 From: vickytechkey Date: Sun, 1 Nov 2026 15:54:59 +0530 Subject: [PATCH] feat: add serializers, SKU generator, and views for seller central API --- api/serializers.py | 29 ++++++++----- api/sku_generator.py | 99 ++++++++++++++++++++++++++++++++++++++++++++ api/views.py | 5 ++- 3 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 api/sku_generator.py diff --git a/api/serializers.py b/api/serializers.py index cdda8cf..5bc2bd0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -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): diff --git a/api/sku_generator.py b/api/sku_generator.py new file mode 100644 index 0000000..02666e1 --- /dev/null +++ b/api/sku_generator.py @@ -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 diff --git a/api/views.py b/api/views.py index 75ff417..25cd6e2 100644 --- a/api/views.py +++ b/api/views.py @@ -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: