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="کدتخفیف") special_discount_code = models.ForeignKey( SpecialDiscountCode, 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}' 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})' # @property def total_price_before_discount(self): return self.price * self.quantity # @property def total_product_discount_amount(self): if self.discount_percent > 0: return int((self.price * self.quantity) * (self.discount_percent / 100)) return 0 # @property def price_after_special_discount(self): all_discounts = (self.special_discount_amount or 0) + self.total_product_discount_amount() print(all_discounts) return self.total_price_before_discount() - all_discounts def unit_price(self): return self.price 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') daily_report = models.ForeignKey('ShopDailyReport', on_delete=models.SET_NULL, null=True, blank=True, related_name='shop_orders', verbose_name='گزارش روزانه') # 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_code = models.ForeignKey( 'account.SpecialDiscountCode', on_delete=models.PROTECT, null=True, blank=True, 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) is_settled = models.BooleanField(default=False, verbose_name='تسویه شده') 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}'