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>
182 lines
6.8 KiB
Python
182 lines
6.8 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
|
|
from apps.catalog.models import Product
|
|
from apps.stores.models import Store
|
|
from utils.models import BaseModel
|
|
|
|
|
|
class Cart(BaseModel):
|
|
"""Pre-checkout state — one per user, may hold items from multiple stores."""
|
|
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart')
|
|
|
|
class Meta:
|
|
verbose_name = 'سبد خرید'
|
|
verbose_name_plural = 'سبدهای خرید'
|
|
|
|
def __str__(self):
|
|
return f'سبد خرید {self.user}'
|
|
|
|
|
|
class CartItem(BaseModel):
|
|
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
|
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='+')
|
|
quantity = models.PositiveIntegerField(default=1)
|
|
|
|
class Meta:
|
|
unique_together = ('cart', 'product')
|
|
verbose_name = 'قلم سبد خرید'
|
|
verbose_name_plural = 'اقلام سبد خرید'
|
|
|
|
def __str__(self):
|
|
return f'{self.product.name} x{self.quantity}'
|
|
|
|
@property
|
|
def line_total(self):
|
|
return self.product.price * self.quantity
|
|
|
|
|
|
class OrderGroup(BaseModel):
|
|
"""One checkout/payment event — fans out into one Order per store (multi-store-cart)."""
|
|
|
|
class DeliveryType(models.TextChoices):
|
|
EXPRESS = 'express', 'ارسال فوری'
|
|
SCHEDULED = 'scheduled', 'زمانبندی شده'
|
|
|
|
class PaymentMethod(models.TextChoices):
|
|
WALLET = 'wallet', 'کیف پول وینسو'
|
|
ONLINE = 'online', 'درگاه بانکی'
|
|
CASH_ON_DELIVERY = 'cash_on_delivery', 'پرداخت در محل'
|
|
|
|
class PaymentStatus(models.TextChoices):
|
|
PENDING = 'pending', 'در انتظار پرداخت'
|
|
PAID = 'paid', 'پرداخت شده'
|
|
FAILED = 'failed', 'ناموفق'
|
|
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='order_groups')
|
|
|
|
recipient_name = models.CharField(max_length=150)
|
|
recipient_phone = models.CharField(max_length=20)
|
|
full_address = models.TextField()
|
|
|
|
delivery_type = models.CharField(max_length=10, choices=DeliveryType.choices, default=DeliveryType.EXPRESS)
|
|
scheduled_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
payment_method = models.CharField(max_length=20, choices=PaymentMethod.choices)
|
|
payment_status = models.CharField(max_length=10, choices=PaymentStatus.choices, default=PaymentStatus.PENDING)
|
|
|
|
notes = models.TextField(blank=True)
|
|
total_amount = models.PositiveBigIntegerField()
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'گروه سفارش'
|
|
verbose_name_plural = 'گروههای سفارش'
|
|
|
|
def __str__(self):
|
|
return f'گروه سفارش {self.uuid} - {self.user}'
|
|
|
|
|
|
class Order(BaseModel):
|
|
class Status(models.TextChoices):
|
|
PLACED = 'placed', 'سفارش ثبت شد'
|
|
PREPARING = 'preparing', 'در حال آمادهسازی'
|
|
READY_TO_SHIP = 'ready_to_ship', 'آماده ارسال'
|
|
HANDED_TO_COURIER = 'handed_to_courier', 'تحویل سفیر شد'
|
|
DELIVERED = 'delivered', 'تحویل داده شد'
|
|
CANCELLED = 'cancelled', 'لغو شده'
|
|
|
|
CANCELLABLE_STATUSES = (Status.PLACED, Status.PREPARING)
|
|
|
|
group = models.ForeignKey(OrderGroup, on_delete=models.PROTECT, related_name='orders')
|
|
store = models.ForeignKey(Store, on_delete=models.PROTECT, related_name='orders')
|
|
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders')
|
|
|
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PLACED)
|
|
|
|
items_subtotal = models.PositiveBigIntegerField()
|
|
delivery_fee = models.PositiveBigIntegerField(default=0)
|
|
commission_amount = models.PositiveBigIntegerField(default=0)
|
|
seller_payout_amount = models.PositiveBigIntegerField(default=0)
|
|
|
|
cancel_reason = models.TextField(blank=True)
|
|
courier_name = models.CharField(max_length=150, blank=True)
|
|
courier_phone = models.CharField(max_length=20, blank=True)
|
|
|
|
placed_at = models.DateTimeField(auto_now_add=True)
|
|
delivered_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'سفارش'
|
|
verbose_name_plural = 'سفارشها'
|
|
|
|
def __str__(self):
|
|
return f'سفارش {self.uuid} - {self.store.name}'
|
|
|
|
@property
|
|
def total_amount(self):
|
|
return self.items_subtotal + self.delivery_fee
|
|
|
|
|
|
class OrderItem(BaseModel):
|
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
|
|
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, related_name='+')
|
|
product_name_snapshot = models.CharField(max_length=200)
|
|
unit_price_snapshot = models.PositiveBigIntegerField()
|
|
quantity = models.PositiveIntegerField()
|
|
|
|
class Meta:
|
|
verbose_name = 'قلم سفارش'
|
|
verbose_name_plural = 'اقلام سفارش'
|
|
|
|
def __str__(self):
|
|
return f'{self.product_name_snapshot} x{self.quantity}'
|
|
|
|
@property
|
|
def line_total(self):
|
|
return self.unit_price_snapshot * self.quantity
|
|
|
|
|
|
class OrderStatusLog(BaseModel):
|
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='status_logs')
|
|
from_status = models.CharField(max_length=20, blank=True)
|
|
to_status = models.CharField(max_length=20)
|
|
changed_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='+',
|
|
)
|
|
note = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['created_at']
|
|
verbose_name = 'تاریخچه وضعیت سفارش'
|
|
verbose_name_plural = 'تاریخچه وضعیت سفارشها'
|
|
|
|
def __str__(self):
|
|
return f'{self.order_id}: {self.from_status} -> {self.to_status}'
|
|
|
|
|
|
class Notification(BaseModel):
|
|
class Type(models.TextChoices):
|
|
NEW_ORDER = 'new_order', 'سفارش جدید'
|
|
SETTLEMENT_DONE = 'settlement_done', 'تسویه حساب'
|
|
NEW_REVIEW = 'new_review', 'نظر جدید'
|
|
ORDER_CANCELLED = 'order_cancelled', 'لغو سفارش'
|
|
|
|
recipient = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notifications')
|
|
type = models.CharField(max_length=20, choices=Type.choices)
|
|
title = models.CharField(max_length=200)
|
|
body = models.TextField(blank=True)
|
|
related_order = models.ForeignKey(
|
|
Order, on_delete=models.SET_NULL, null=True, blank=True, related_name='notifications',
|
|
)
|
|
is_read = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'اعلان'
|
|
verbose_name_plural = 'اعلانها'
|
|
|
|
def __str__(self):
|
|
return f'{self.title} - {self.recipient}'
|