Files
hossein-por-shop/backend/order/models.py
T
2025-12-11 13:14:46 +03:30

345 lines
15 KiB
Python

from account.models import SpecialDiscountCode
from django.db import models, transaction
from account.models import User, UserAddressModel, PushSubscription
from product.models import ProductModel, ProductVariant, ProductImageModel
from django.utils import timezone
from django_jalali.db import models as jmodels
from django.core.exceptions import ValidationError
from django.conf import settings
class DiscountCode(models.Model):
code = models.CharField(max_length=50, verbose_name='کد تخفیف')
percent = models.DecimalField(
max_digits=4, decimal_places=2, verbose_name='درصد')
quantity = models.PositiveIntegerField(verbose_name='تعداد')
expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا')
def __str__(self):
return self.code
class Meta:
verbose_name = 'کد تخفیف'
verbose_name_plural = 'کد های تخفیف'
def is_valid(self):
return self.expiration_date > timezone.now() and self.quantity > 0
def not_valid_reason(self):
if self.expiration_date > timezone.now() and self.quantity > 0:
return 'این کد معتبر میباشد'
elif not self.expiration_date > timezone.now():
return 'تایم کد تخفیف تمام شده'
elif not self.quantity > 0:
return 'این کد تخفیف تمام شده است'
else:
print('log later bug')
class Cart(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='carts'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL,
related_name='carts', null=True, verbose_name='ادرس')
discount_code = models.ForeignKey(
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
special_discount_code = models.ForeignKey(
SpecialDiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف خاص")
class Meta:
verbose_name = 'سبد خرید'
verbose_name_plural = 'سبد های خرید'
def __str__(self):
return f"Cart for {self.user.email}"
def clear_cart(self):
self.items.all().delete()
self.discount_code = None
self.special_discount_code = None
self.save()
@property
def discount_code_amount(self):
if self.discount_code:
return int(int(self.cart_total - self.items_discount_amount) * (self.discount_code.percent / 100))
else:
return 0
@property
def items_discount_amount(self):
return int(sum(item.item_discount_amount for item in self.items.all()))
@property
def special_discount_total(self):
"""Sum of all special discounts from cart items when special_discount_code is applied."""
if self.special_discount_code:
return int(sum(item.item_special_discount_amount for item in self.items.all()))
return 0
@property
def total_before_tax(self):
return self.cart_total - (self.discount_code_amount + self.items_discount_amount + self.special_discount_total)
@property
def tax_amount(self):
return int(self.total_before_tax * settings.DEFAULT_TAX_RATE / 100)
@property
def cart_total(self):
return sum(item.price_before_discount for item in self.items.all())
@property
def final_price(self):
return self.total_before_tax + self.tax_amount
class CartItem(models.Model):
cart = models.ForeignKey(
Cart,
on_delete=models.CASCADE,
related_name='items'
)
product_variant = models.ForeignKey(
ProductVariant,
on_delete=models.CASCADE,
related_name='cart_items'
)
quantity = models.PositiveIntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# special_discount_amount = models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه', help_text='تخفیف محاسبه شده از سود تنوع')
class Meta:
verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید'
unique_together = ('cart', 'product_variant')
def __str__(self):
return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}"
@property
def special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@property
def price_before_discount(self):
return self.quantity * self.product_variant.price_before_discount
@property
def item_discount_amount(self):
return self.product_variant.discount_amount * self.quantity
@property
def item_special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@property
def price_after_discount(self):
return self.price_before_discount - self.item_discount_amount - self.item_special_discount_amount
@property
def discount(self):
return self.product_variant.discount
class OrderModel(models.Model):
objects = jmodels.jManager()
STATUS_CHOICES = [
('ADMIN_PENDING', 'در انتظار تایید'),
('PENDING', 'درحال پردازش'),
('POSTED', 'ارسال شده'),
('RECEIVED', 'تحویل شده'),
('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'),
]
user = models.ForeignKey(User, on_delete=models.SET_NULL,
null=True, related_name='orders', verbose_name='کاربر')
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL,
related_name='orders', null=True, verbose_name='ادرس')
created_at = jmodels.jDateField(
blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت")
discount_code = models.ForeignKey(
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
status = models.CharField(max_length=20, choices=STATUS_CHOICES,
default='ADMIN_PENDING', verbose_name="وضعیت سفارش")
discount_amount = models.BigIntegerField(
null=True, blank=True, verbose_name='مقدار کد تخفیف')
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
final_price = models.BigIntegerField(
null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(
null=True, blank=True, verbose_name='کل سبد خرید')
special_discount_total = models.BigIntegerField(
null=True, blank=True, verbose_name='مجموع تخفیف ویژه')
cart = models.ForeignKey(
Cart, on_delete=models.CASCADE, null=True, blank=True)
is_stock_rolled_back = models.BooleanField(
default=False, verbose_name="موجودی برگردانده شده")
def __str__(self):
return f'سفارش: {self.pk + 1000}'
class Meta:
verbose_name = 'سفارش'
verbose_name_plural = 'سفارشات'
def rollback_stock(self):
"""
Rollback stock quantities for all items in this order
Returns True if successful, False otherwise
"""
if self.is_stock_rolled_back:
return False
# if not self.cart:
# return False
try:
# Get all cart items and rollback their stock
for order_item in self.items.all():
product = order_item.product
# Add back the quantity to stock
product.in_stock += order_item.quantity
product.save()
# Mark as rolled back
self.is_stock_rolled_back = True
self.save(update_fields=['is_stock_rolled_back'])
self.status = 'CANCELED'
self.save()
return True
except Exception as e:
print(e)
# Log the error if you have logging setup
# logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
return False
class OrderItemModel(models.Model):
order = models.ForeignKey(
OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.BigIntegerField(verbose_name='قیمت')
product = models.ForeignKey(
ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
special_discount_amount = models.BigIntegerField(
default=0, verbose_name='مقدار تخفیف ویژه')
class Meta:
verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید'
def __str__(self):
return f'({self.product}) - ({self.order.user})'
class ShopOrderModel(models.Model):
"""Represents the portion of a customer Order that belongs to a single Shop.
This model is created automatically when an `OrderModel` is finalized/paid.
It holds aggregated financial details for that shop (subtotal, discounts,
commission and final payable amount).
"""
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='shop_orders')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE, related_name='shop_orders')
# Customer Information
customer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='مشتری')
customer_phone = models.CharField(max_length=12, verbose_name='شماره تلفن مشتری', blank=True)
customer_name = models.CharField(max_length=100, verbose_name='نام مشتری', blank=True)
# Delivery Address (ForeignKey + text backup)
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='آدرس تحویل')
address_text = models.TextField(verbose_name='آدرس کامل', blank=True)
address_postal_code = models.CharField(max_length=10, verbose_name='کد پستی', blank=True)
address_phone = models.CharField(max_length=11, verbose_name='شماره تماس تحویل گیرنده', blank=True)
address_city = models.CharField(max_length=30, verbose_name='شهر', blank=True)
address_province = models.CharField(max_length=30, verbose_name='استان', blank=True)
address_recipient_name = models.CharField(max_length=100, verbose_name='نام تحویل گیرنده', blank=True)
# Order Status & Payment
STATUS_CHOICES = [
('ADMIN_PENDING', 'در انتظار تایید'),
('PENDING', 'درحال پردازش'),
('POSTED', 'ارسال شده'),
('RECEIVED', 'تحویل شده'),
('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ADMIN_PENDING', verbose_name='وضعیت سفارش')
is_paid = models.BooleanField(default=False, verbose_name='وضعیت پرداخت')
# Financial Details
subtotal = models.BigIntegerField(verbose_name='جمع جزئیات', default=0)
items_count = models.PositiveIntegerField(default=0)
discount_amount = models.BigIntegerField(default=0, verbose_name='تخفیف اختصاصی فروشگاه')
special_discount_amount = models.BigIntegerField(default=0, verbose_name='تخفیف ویژه اختصاصی')
commission_percent = models.DecimalField(max_digits=5, decimal_places=2, verbose_name='درصد کمیسیون')
commission_amount = models.BigIntegerField(default=0, verbose_name='مبلغ کمیسیون')
tax_amount = models.BigIntegerField(default=0, verbose_name='مالیات')
payable_amount = models.BigIntegerField(default=0, verbose_name='قابل پرداخت به فروشگاه')
is_settled = models.BooleanField(default=False, verbose_name='تسویه شده')
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
order_created_at = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ ثبت سفارش اصلی')
class Meta:
verbose_name = 'سفارش به ازای فروشگاه'
verbose_name_plural = 'سفارشات به ازای فروشگاه'
def __str__(self):
return f'ShopOrder({self.shop.shop_name}) for Order {self.order.pk}'
class ShopOrderItem(models.Model):
shop_order = models.ForeignKey(ShopOrderModel, on_delete=models.CASCADE, related_name='items')
order_item = models.ForeignKey(OrderItemModel, on_delete=models.PROTECT, related_name='shop_items')
quantity = models.PositiveIntegerField()
unit_price = models.BigIntegerField()
total_price = models.BigIntegerField()
discount_amount = models.BigIntegerField(default=0)
special_discount_amount = models.BigIntegerField(default=0)
class Meta:
verbose_name = 'ایتم سفارش فروشگاه'
verbose_name_plural = 'ایتم های سفارش فروشگاه'
def __str__(self):
return f'{self.order_item} for {self.shop_order}'
class ShopDailyReport(models.Model):
"""Daily aggregated report per shop. Run once per day to record totals."""
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE, related_name='daily_reports')
date = models.DateField()
total_sales = models.BigIntegerField(default=0)
total_commission = models.BigIntegerField(default=0)
total_payable = models.BigIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'گزارش روزانه فروشگاه'
verbose_name_plural = 'گزارش های روزانه فروشگاه'
unique_together = (('shop', 'date'),)
def __str__(self):
return f'Report {self.shop.shop_name} - {self.date}'