345 lines
15 KiB
Python
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}'
|