All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 46s
431 lines
21 KiB
Python
431 lines
21 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
|
|
class Category(models.Model):
|
|
name = models.CharField(max_length=100, unique=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def generate_seller_mid():
|
|
"""
|
|
Generates the next unique Seller Merchant ID (MID) in the format TRDPX000001.
|
|
"""
|
|
import re
|
|
existing_mids = list(SupplierProfile.objects.filter(merchant_id__startswith='TRDPX').values_list('merchant_id', flat=True))
|
|
draft_mids = list(SupplierOnboardingDraft.objects.filter(merchant_id__startswith='TRDPX').values_list('merchant_id', flat=True))
|
|
|
|
max_num = 0
|
|
for mid in existing_mids + draft_mids:
|
|
if mid and mid.startswith('TRDPX'):
|
|
match = re.search(r'TRDPX(\d+)', mid)
|
|
if match:
|
|
try:
|
|
num = int(match.group(1))
|
|
if num > max_num:
|
|
max_num = num
|
|
except ValueError:
|
|
pass
|
|
|
|
next_num = max_num + 1
|
|
new_mid = f"TRDPX{next_num:06d}"
|
|
|
|
while SupplierProfile.objects.filter(merchant_id=new_mid).exists() or SupplierOnboardingDraft.objects.filter(merchant_id=new_mid).exists():
|
|
next_num += 1
|
|
new_mid = f"TRDPX{next_num:06d}"
|
|
|
|
return new_mid
|
|
|
|
|
|
class SupplierProfile(models.Model):
|
|
STATUS_CHOICES = [
|
|
('unverified', 'Unverified'),
|
|
('phone_verified', 'Phone Verified'),
|
|
('email_verified', 'Email Verified'),
|
|
('documents_submitted', 'Documents Submitted'),
|
|
('pending_approval', 'Pending Approval'),
|
|
('approved', 'Approved'),
|
|
('rejected', 'Rejected'),
|
|
('suspended', 'Suspended'),
|
|
]
|
|
BUSINESS_TYPE_CHOICES = [
|
|
('registered_company', 'Registered Company'),
|
|
('self_help_group', 'Self Help Group'),
|
|
('individual_maker', 'Individual Maker'),
|
|
]
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
|
merchant_id = models.CharField(max_length=50, unique=True, blank=True, null=True, db_index=True, help_text="Unique Seller Merchant ID, e.g. TRDPX000001")
|
|
phone = models.CharField(max_length=20, blank=True, null=True)
|
|
phone_verified = models.BooleanField(default=False)
|
|
email_verified = models.BooleanField(default=False)
|
|
gstin = models.CharField(max_length=15, blank=True, null=True)
|
|
is_gstin_verified = models.BooleanField(default=False)
|
|
pan_number = models.CharField(max_length=20, blank=True, null=True)
|
|
is_pan_verified = models.BooleanField(default=False)
|
|
verified_pan_name = models.CharField(max_length=255, blank=True, null=True)
|
|
verified_gst_name = models.CharField(max_length=255, blank=True, null=True)
|
|
is_verified = models.BooleanField(default=False, help_text="True if PAN and GST full names match and seller identity is confirmed")
|
|
business_type = models.CharField(max_length=50, choices=BUSINESS_TYPE_CHOICES, blank=True, null=True)
|
|
store_slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
|
|
categories = models.ManyToManyField(Category, blank=True, related_name='suppliers')
|
|
aadhar_file = models.CharField(max_length=255, blank=True, null=True) # retaining old files or S3 keys
|
|
pan_file = models.CharField(max_length=255, blank=True, null=True)
|
|
aadhar_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
pan_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
store_name = models.CharField(max_length=255, blank=True, null=True)
|
|
store_logo = models.TextField(blank=True, null=True)
|
|
logo_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
business_bio = models.TextField(blank=True, null=True)
|
|
street = models.CharField(max_length=255, blank=True, null=True)
|
|
city = models.CharField(max_length=100, blank=True, null=True)
|
|
state = models.CharField(max_length=100, blank=True, null=True)
|
|
pincode = models.CharField(max_length=20, blank=True, null=True)
|
|
latitude = models.FloatField(default=12.9716)
|
|
longitude = models.FloatField(default=77.5946)
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='approved')
|
|
submitted_at = models.DateTimeField(blank=True, null=True)
|
|
approved_at = models.DateTimeField(blank=True, null=True)
|
|
approved_by = models.CharField(max_length=150, blank=True, null=True)
|
|
rejected_reason = models.TextField(blank=True, null=True)
|
|
is_profile_complete = models.BooleanField(default=False)
|
|
pickup_code = models.CharField(max_length=100, blank=True, null=True, help_text='Shadowfax Seller Pickup Code')
|
|
|
|
# Bank Details
|
|
account_holder_name = models.CharField(max_length=255, blank=True, null=True)
|
|
account_number = models.CharField(max_length=50, blank=True, null=True)
|
|
ifsc_code = models.CharField(max_length=20, blank=True, null=True)
|
|
bank_name = models.CharField(max_length=255, blank=True, null=True)
|
|
is_bank_verified = models.BooleanField(default=False)
|
|
|
|
# Tax & Legal Details
|
|
tax_mode = models.CharField(max_length=20, default='gstin')
|
|
enrolment_id = models.CharField(max_length=50, blank=True, null=True)
|
|
legal_entity_name = models.CharField(max_length=255, blank=True, null=True)
|
|
trade_name = models.CharField(max_length=255, blank=True, null=True)
|
|
exemption_reason = models.TextField(blank=True, null=True)
|
|
|
|
# Business Contact & Extended Address
|
|
business_email = models.EmailField(blank=True, null=True)
|
|
support_phone = models.CharField(max_length=20, blank=True, null=True)
|
|
address_line2 = models.CharField(max_length=255, blank=True, null=True)
|
|
landmark = models.CharField(max_length=255, blank=True, null=True)
|
|
|
|
# Storefront Customization
|
|
banner_url = models.TextField(blank=True, null=True)
|
|
banner_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
craft_lineage_years = models.IntegerField(default=0)
|
|
workshop_location = models.CharField(max_length=255, blank=True, null=True)
|
|
artisan_avatar = models.TextField(blank=True, null=True)
|
|
featured_tagline = models.CharField(max_length=255, blank=True, null=True)
|
|
social_links = models.JSONField(default=dict, blank=True, null=True)
|
|
|
|
def __block__(self):
|
|
return
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.merchant_id:
|
|
draft = getattr(self.user, 'onboarding_draft', None) if hasattr(self, 'user') else None
|
|
if draft and draft.merchant_id:
|
|
self.merchant_id = draft.merchant_id
|
|
else:
|
|
self.merchant_id = generate_seller_mid()
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username}'s Profile"
|
|
|
|
|
|
class SupplierOnboardingDraft(models.Model):
|
|
"""
|
|
Intermediate model holding incomplete onboarding draft records.
|
|
SupplierProfile (api_supplierprofile) must NEVER contain incomplete onboarding records.
|
|
Only when onboarding is completed (is_profile_complete = True) is SupplierProfile created.
|
|
"""
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='onboarding_draft')
|
|
merchant_id = models.CharField(max_length=50, blank=True, null=True, db_index=True, help_text="Unique Seller Merchant ID, e.g. TRDPX000001")
|
|
phone = models.CharField(max_length=20, blank=True, null=True)
|
|
phone_verified = models.BooleanField(default=False)
|
|
email_verified = models.BooleanField(default=False)
|
|
|
|
# Step 2: GST / PAN / Business
|
|
tax_mode = models.CharField(max_length=20, default='gstin')
|
|
business_type = models.CharField(max_length=50, blank=True, null=True)
|
|
pan_number = models.CharField(max_length=20, blank=True, null=True)
|
|
is_pan_verified = models.BooleanField(default=False)
|
|
verified_pan_name = models.CharField(max_length=255, blank=True, null=True)
|
|
gstin = models.CharField(max_length=15, blank=True, null=True)
|
|
is_gstin_verified = models.BooleanField(default=False)
|
|
verified_gst_name = models.CharField(max_length=255, blank=True, null=True)
|
|
is_seller_verified = models.BooleanField(default=False)
|
|
is_verified = models.BooleanField(default=False)
|
|
enrolment_id = models.CharField(max_length=50, blank=True, null=True)
|
|
legal_entity_name = models.CharField(max_length=255, blank=True, null=True)
|
|
trade_name = models.CharField(max_length=255, blank=True, null=True)
|
|
exemption_reason = models.TextField(blank=True, null=True)
|
|
|
|
# Step 3: Address & Documents
|
|
aadhar_file = models.CharField(max_length=255, blank=True, null=True)
|
|
pan_file = models.CharField(max_length=255, blank=True, null=True)
|
|
aadhar_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
pan_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
street = models.CharField(max_length=255, blank=True, null=True)
|
|
address_line2 = models.CharField(max_length=255, blank=True, null=True)
|
|
city = models.CharField(max_length=100, blank=True, null=True)
|
|
state = models.CharField(max_length=100, blank=True, null=True)
|
|
pincode = models.CharField(max_length=20, blank=True, null=True)
|
|
landmark = models.CharField(max_length=255, blank=True, null=True)
|
|
latitude = models.FloatField(default=12.9716)
|
|
longitude = models.FloatField(default=77.5946)
|
|
|
|
# Step 4: Bank Details
|
|
account_holder_name = models.CharField(max_length=255, blank=True, null=True)
|
|
account_number = models.CharField(max_length=50, blank=True, null=True)
|
|
ifsc_code = models.CharField(max_length=20, blank=True, null=True)
|
|
bank_name = models.CharField(max_length=255, blank=True, null=True)
|
|
is_bank_verified = models.BooleanField(default=False)
|
|
|
|
# Step 5 & 6: Categories & Initial Products
|
|
categories = models.JSONField(default=list, blank=True)
|
|
sample_product_data = models.JSONField(default=dict, blank=True)
|
|
|
|
# Step 7: Store Profile & Lineage
|
|
store_name = models.CharField(max_length=255, blank=True, null=True)
|
|
store_slug = models.SlugField(max_length=255, blank=True, null=True)
|
|
store_logo = models.TextField(blank=True, null=True)
|
|
logo_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
banner_url = models.TextField(blank=True, null=True)
|
|
banner_s3_key = models.CharField(max_length=255, blank=True, null=True)
|
|
featured_tagline = models.CharField(max_length=255, blank=True, null=True)
|
|
business_bio = models.TextField(blank=True, null=True)
|
|
workshop_location = models.CharField(max_length=255, blank=True, null=True)
|
|
artisan_avatar = models.TextField(blank=True, null=True)
|
|
craft_lineage_years = models.IntegerField(default=0)
|
|
social_links = models.JSONField(default=dict, blank=True, null=True)
|
|
|
|
# Contact
|
|
business_email = models.EmailField(blank=True, null=True)
|
|
support_phone = models.CharField(max_length=20, blank=True, null=True)
|
|
|
|
# Tracking
|
|
onboarding_step = models.IntegerField(default=1)
|
|
draft_data = models.JSONField(default=dict, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'api_supplieronboardingdraft'
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.merchant_id:
|
|
self.merchant_id = generate_seller_mid()
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username}'s Onboarding Draft (Step {self.onboarding_step})"
|
|
|
|
class SellerKYC(models.Model):
|
|
supplier = models.OneToOneField(User, on_delete=models.CASCADE, related_name='kyc')
|
|
aadhaar_number = models.CharField(max_length=20, blank=True, null=True)
|
|
name = models.CharField(max_length=255, blank=True, null=True)
|
|
dob = models.CharField(max_length=50, blank=True, null=True)
|
|
gender = models.CharField(max_length=10, blank=True, null=True)
|
|
address = models.TextField(blank=True, null=True)
|
|
face_base64 = models.TextField(blank=True, null=True)
|
|
verification_response = models.JSONField(default=dict, blank=True, null=True)
|
|
|
|
pan_number = models.CharField(max_length=20, blank=True, null=True)
|
|
pan_full_name = models.CharField(max_length=255, blank=True, null=True)
|
|
pan_dob = models.CharField(max_length=50, blank=True, null=True)
|
|
pan_status = models.CharField(max_length=50, blank=True, null=True)
|
|
pan_aadhaar_linked = models.BooleanField(default=False)
|
|
pan_verification_response = models.JSONField(default=dict, blank=True, null=True)
|
|
is_pan_verified = models.BooleanField(default=False)
|
|
|
|
gst_number = models.CharField(max_length=20, blank=True, null=True)
|
|
gst_legal_name = models.CharField(max_length=255, blank=True, null=True)
|
|
gst_status = models.CharField(max_length=50, blank=True, null=True)
|
|
gst_verification_response = models.JSONField(default=dict, blank=True, null=True)
|
|
is_gst_verified = models.BooleanField(default=False)
|
|
|
|
is_seller_verified = models.BooleanField(default=False)
|
|
verification_data = models.JSONField(default=dict, blank=True, null=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.supplier.username} - KYC"
|
|
|
|
class SellerBankVerification(models.Model):
|
|
"""
|
|
Dedicated bank verification record for seller onboarding and payouts,
|
|
keyed by seller_id as primary key.
|
|
"""
|
|
seller_id = models.CharField(max_length=100, primary_key=True, help_text="Unique Seller / Supplier ID")
|
|
account_number = models.CharField(max_length=50)
|
|
ifsc = models.CharField(max_length=20)
|
|
account_ifsc = models.CharField(max_length=20, blank=True, null=True)
|
|
beneficiary_name = models.CharField(max_length=255, blank=True, null=True)
|
|
is_valid = models.BooleanField(default=False)
|
|
account_exists = models.BooleanField(default=False)
|
|
failure_reason = models.TextField(blank=True, null=True)
|
|
npci_error_code = models.CharField(max_length=100, blank=True, null=True)
|
|
tranx_id = models.CharField(max_length=100, blank=True, null=True, db_index=True)
|
|
timestamp = models.CharField(max_length=100, blank=True, null=True)
|
|
user_consent = models.CharField(max_length=50, default="true")
|
|
vendor_name = models.CharField(max_length=50, default="codeplus")
|
|
status = models.CharField(max_length=50, default="PENDING")
|
|
latency_ms = models.FloatField(default=0.0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "seller_bank_verification"
|
|
verbose_name = "Seller Bank Verification"
|
|
verbose_name_plural = "Seller Bank Verifications"
|
|
|
|
def __str__(self):
|
|
return f"Seller {self.seller_id} Bank: {self.beneficiary_name or self.account_number} ({'Valid' if self.is_valid else 'Invalid'})"
|
|
|
|
|
|
class OTPRecord(models.Model):
|
|
TYPE_CHOICES = [
|
|
('phone', 'Phone'),
|
|
('email', 'Email'),
|
|
('password_reset', 'Password Reset'),
|
|
]
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='otp_records')
|
|
type = models.CharField(max_length=20, choices=TYPE_CHOICES)
|
|
otp_hash = models.CharField(max_length=64) # SHA-256 hash
|
|
expires_at = models.DateTimeField()
|
|
attempts = models.IntegerField(default=0)
|
|
used = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.type} OTP"
|
|
|
|
class BulkUploadLog(models.Model):
|
|
STATUS_CHOICES = [
|
|
('parsing', 'Parsing'),
|
|
('validating', 'Validating'),
|
|
('completed', 'Completed'),
|
|
('failed', 'Failed'),
|
|
]
|
|
supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bulk_upload_logs', db_constraint=False)
|
|
log_id = models.CharField(max_length=50, unique=True)
|
|
s3_key = models.CharField(max_length=255)
|
|
total_rows = models.IntegerField(default=0)
|
|
rows_created = models.IntegerField(default=0)
|
|
rows_updated = models.IntegerField(default=0)
|
|
rows_skipped = models.IntegerField(default=0)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='parsing')
|
|
error_summary = models.TextField(blank=True, null=True) # JSON stored as string
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
completed_at = models.DateTimeField(blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.log_id
|
|
|
|
|
|
class Product(models.Model):
|
|
supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products', db_constraint=False)
|
|
title = models.CharField(max_length=255)
|
|
category = models.CharField(max_length=100)
|
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
stock = models.IntegerField(default=0)
|
|
sku = models.CharField(max_length=100, unique=True)
|
|
image = models.TextField(blank=True, null=True) # Can store URL or base64
|
|
status = models.CharField(max_length=50, default='pending')
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
# New heritage fields from UI designs
|
|
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)
|
|
additional_images = models.JSONField(default=list, blank=True, null=True)
|
|
video_url = models.URLField(blank=True, null=True)
|
|
view_360_url = models.URLField(blank=True, null=True)
|
|
compare_at_price = models.DecimalField(max_digits=10, decimal_places=2, 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)
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
class Order(models.Model):
|
|
STATUS_CHOICES = [
|
|
('Pending Acceptance', 'Pending Acceptance'),
|
|
('Ready to Ship', 'Ready to Ship'),
|
|
('Shipped', 'Shipped'),
|
|
('Delivered', 'Delivered'),
|
|
('Cancelled', 'Cancelled'),
|
|
('Rejected', 'Rejected'),
|
|
]
|
|
supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders', db_constraint=False)
|
|
order_id = models.CharField(max_length=50, unique=True)
|
|
date = models.DateField()
|
|
item = models.CharField(max_length=255)
|
|
quantity = models.IntegerField(default=1)
|
|
customer = models.CharField(max_length=255)
|
|
total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Pending Acceptance')
|
|
carrier = models.CharField(max_length=100, default='Pending')
|
|
tracking = models.CharField(max_length=100, default='Pending')
|
|
payment_mode = models.CharField(max_length=20, default='Prepaid')
|
|
eta = models.CharField(max_length=50, default='N/A')
|
|
expected_delivery_date = models.CharField(max_length=100, blank=True, null=True, help_text='Logistics Expected Delivery Date')
|
|
pickup_id = models.CharField(max_length=100, blank=True, null=True, help_text='Carrier Pickup Reference ID')
|
|
pickup_payload = models.JSONField(default=dict, blank=True, null=True, help_text='Carrier Pickup Scheduling Response & Audit Details')
|
|
|
|
def __str__(self):
|
|
return self.order_id
|
|
|
|
class ReturnRequest(models.Model):
|
|
STATUS_CHOICES = [
|
|
('Pending Approval', 'Pending Approval'),
|
|
('Approved', 'Approved'),
|
|
('Rejected', 'Rejected'),
|
|
('In Transit', 'In Transit'),
|
|
]
|
|
supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='returns', db_constraint=False)
|
|
return_id = models.CharField(max_length=50, unique=True)
|
|
order_id = models.CharField(max_length=50)
|
|
customer = models.CharField(max_length=255)
|
|
item = models.CharField(max_length=255)
|
|
reason = models.TextField()
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Pending Approval')
|
|
image = models.TextField(blank=True, null=True)
|
|
returning_tracking = models.CharField(max_length=100, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.return_id
|
|
|
|
class Wallet(models.Model):
|
|
supplier = models.OneToOneField(User, on_delete=models.CASCADE, related_name='wallet', db_constraint=False)
|
|
outstanding = models.DecimalField(max_digits=12, decimal_places=2, default=0.00)
|
|
withdrawn = models.DecimalField(max_digits=12, decimal_places=2, default=0.00)
|
|
|
|
def __str__(self):
|
|
return f"{self.supplier.username}'s Wallet"
|
|
|
|
class WalletTransaction(models.Model):
|
|
STATUS_CHOICES = [
|
|
('Transferred', 'Transferred'),
|
|
('Pending', 'Pending'),
|
|
]
|
|
wallet = models.ForeignKey(Wallet, on_delete=models.CASCADE, related_name='transactions')
|
|
tx_id = models.CharField(max_length=50, unique=True)
|
|
date = models.DateField()
|
|
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Transferred')
|
|
|
|
def __str__(self):
|
|
return self.tx_id
|