Django 6 + DRF resource server against the Gooyal accounts OAuth2 service, matching the Winsoo ecosystem's conventions. Covers locations, stores, catalog, cart, checkout/orders (with the multi-store-cart split and the status stepper), reviews, and notifications, plus a demo-data seed command. Payment integration (wallet debits, online gateway, seller payouts) is intentionally left out here — see feature/payment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
from django.conf import settings
|
|
from django.contrib.gis.db import models as gis_models
|
|
from django.core.validators import RegexValidator
|
|
from django.db import models
|
|
|
|
from apps.locations.models import City, Neighborhood
|
|
from utils.models import BaseModel
|
|
|
|
|
|
class StoreCategory(BaseModel):
|
|
name = models.CharField(max_length=100, unique=True)
|
|
icon = models.ImageField(upload_to='store_categories/', null=True, blank=True)
|
|
order = models.PositiveSmallIntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['order', 'name']
|
|
verbose_name = 'دستهبندی فروشگاه'
|
|
verbose_name_plural = 'دستهبندیهای فروشگاه'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Store(BaseModel):
|
|
class Status(models.TextChoices):
|
|
PENDING = 'pending', 'در انتظار تایید'
|
|
APPROVED = 'approved', 'تایید شده'
|
|
SUSPENDED = 'suspended', 'معلق شده'
|
|
|
|
owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='store')
|
|
category = models.ForeignKey(StoreCategory, on_delete=models.PROTECT, related_name='stores')
|
|
name = models.CharField(max_length=150)
|
|
description = models.TextField(blank=True)
|
|
logo = models.ImageField(upload_to='store_logos/', null=True, blank=True)
|
|
cover_image = models.ImageField(upload_to='store_covers/', null=True, blank=True)
|
|
phone_number = models.CharField(max_length=20, blank=True)
|
|
|
|
city = models.ForeignKey(City, on_delete=models.PROTECT, related_name='stores')
|
|
address = models.TextField()
|
|
location = gis_models.PointField(geography=True, null=True, blank=True)
|
|
service_neighborhoods = models.ManyToManyField(
|
|
Neighborhood, related_name='stores', blank=True,
|
|
help_text='محلههایی که این فروشگاه به آنها ارسال دارد.',
|
|
)
|
|
delivery_radius_km = models.DecimalField(max_digits=5, decimal_places=2, default=5)
|
|
|
|
min_order_amount = models.PositiveBigIntegerField(default=0)
|
|
delivery_fee = models.PositiveBigIntegerField(default=0)
|
|
free_delivery_threshold = models.PositiveBigIntegerField(null=True, blank=True)
|
|
commission_percent = models.DecimalField(
|
|
max_digits=5, decimal_places=2, null=True, blank=True,
|
|
help_text='در صورت خالی بودن، از DEFAULT_COMMISSION_PERCENT استفاده میشود.',
|
|
)
|
|
|
|
accepts_wallet = models.BooleanField(default=True)
|
|
accepts_online = models.BooleanField(default=True)
|
|
accepts_cash_on_delivery = models.BooleanField(default=True)
|
|
|
|
rating_avg = models.DecimalField(max_digits=3, decimal_places=2, default=0)
|
|
rating_count = models.PositiveIntegerField(default=0)
|
|
|
|
status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING)
|
|
is_open = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'فروشگاه'
|
|
verbose_name_plural = 'فروشگاهها'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
@property
|
|
def commission_percent_effective(self):
|
|
if self.commission_percent is not None:
|
|
return self.commission_percent
|
|
from django.conf import settings as dj_settings
|
|
return dj_settings.DEFAULT_COMMISSION_PERCENT
|
|
|
|
|
|
class StoreWorkingHours(BaseModel):
|
|
class Weekday(models.IntegerChoices):
|
|
SATURDAY = 0, 'شنبه'
|
|
SUNDAY = 1, 'یکشنبه'
|
|
MONDAY = 2, 'دوشنبه'
|
|
TUESDAY = 3, 'سهشنبه'
|
|
WEDNESDAY = 4, 'چهارشنبه'
|
|
THURSDAY = 5, 'پنجشنبه'
|
|
FRIDAY = 6, 'جمعه'
|
|
|
|
store = models.ForeignKey(Store, on_delete=models.CASCADE, related_name='working_hours')
|
|
weekday = models.PositiveSmallIntegerField(choices=Weekday.choices)
|
|
opens_at = models.TimeField(null=True, blank=True)
|
|
closes_at = models.TimeField(null=True, blank=True)
|
|
is_closed = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ['weekday']
|
|
unique_together = ('store', 'weekday')
|
|
verbose_name = 'ساعت کاری فروشگاه'
|
|
verbose_name_plural = 'ساعات کاری فروشگاه'
|
|
|
|
def __str__(self):
|
|
return f'{self.store.name} - {self.get_weekday_display()}'
|
|
|
|
|
|
class StoreBankAccount(BaseModel):
|
|
store = models.OneToOneField(Store, on_delete=models.CASCADE, related_name='bank_account')
|
|
iban = models.CharField(
|
|
max_length=26,
|
|
validators=[RegexValidator(r'^IR\d{24}$', 'شماره شبا معتبر نیست.')],
|
|
)
|
|
account_holder_name = models.CharField(max_length=150)
|
|
|
|
class Meta:
|
|
verbose_name = 'حساب بانکی فروشگاه'
|
|
verbose_name_plural = 'حسابهای بانکی فروشگاه'
|
|
|
|
def __str__(self):
|
|
return f'{self.store.name} - {self.iban}'
|