Files
hossein-por-shop/backend/product/models.py
T

481 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from django.utils.text import slugify
from account.models import User
from django.urls import reverse
import requests
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from home.models import ShowCaseSlider
from decimal import Decimal
from dirtyfields import DirtyFieldsMixin
class UnitCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True,
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',
verbose_name='آیکون', blank=True, null=True)
image = models.ImageField(
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
class Meta:
verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌هااصلی"
indexes = [
models.Index(fields=['slug'], name='main_category_slug_idx'),
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = 'unit-category-' + slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True,
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',
verbose_name='آیکون', blank=True, null=True)
image = models.ImageField(
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
video = models.FileField(upload_to='category_videos/',
blank=True, null=True, verbose_name='ویدیو')
parent = models.ForeignKey(UnitCategoryModel, on_delete=models.SET_NULL,
related_name='maincategorys', verbose_name='دسته‌بندی والد', null=True)
class Meta:
verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌هااصلی"
indexes = [
models.Index(fields=['slug'], name='unit_category_slug_idx'),
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = 'category-' + slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class SubCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True,
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
image = models.ImageField(
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
icon = models.ImageField(upload_to='category_model/',
verbose_name='آیکون', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.SET_NULL,
related_name='subcategorys', verbose_name='دسته‌بندی والد', null=True)
class Meta:
verbose_name = "زیر دسته‌بندی"
verbose_name_plural = "زیر دسته‌بندی‌ها"
indexes = [
models.Index(fields=['slug'], name='sub_category_slug_idx'),
models.Index(fields=['parent'], name='sub_category_parent_idx'),
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
# Ensure slug is present and unique. If a slug (auto or custom) would
# collide with an existing product, append a numeric suffix.
if not self.slug:
base_slug = slugify(self.name, allow_unicode=True)
else:
# Normalize a provided slug
base_slug = slugify(self.slug, allow_unicode=True)
slug_candidate = base_slug
counter = 1
# Exclude self.pk to allow updating the same instance
while ProductModel.objects.filter(slug=slug_candidate).exclude(pk=self.pk).exists():
slug_candidate = f"{base_slug}-{counter}"
counter += 1
self.slug = slug_candidate
super().save(*args, **kwargs)
class DollorModel(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='قیمت دلار')
defualt_price = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True, default=80000.0, verbose_name='قیمت دستی')
# these fields will avoid dublicate of this model
unique = (('unique', 'unique'),)
unique_filed = models.CharField(
max_length=20, choices=unique, unique=True, default='unique')
def __str__(self):
return str(self.price)
def save(self, *args, **kwargs):
if not self.price:
self.update_price()
super().save(*args, **kwargs)
def update_price(self):
self.price = self.get_usd_price()
def get_usd_price(self):
try:
api_usd = "https://api.nobitex.ir/v2/orderbook/USDTIRT"
response = requests.get(api_usd)
data = response.json()
price = int(data["lastTradePrice"])
price_in_usd = price / 10.0
print('\n\nprice from api \n\n')
except Exception as e:
return self.defualt_price
return price_in_usd
class Meta:
verbose_name = 'مدل دلار'
verbose_name_plural = 'مدل دلار'
indexes = [
models.Index(fields=['unique_filed'],
name='dollor_unique_field_idx'),
]
class InPackItems(models.Model):
item_title = models.CharField(max_length=50)
cover = models.ImageField(
upload_to='product_items/', verbose_name='کاور ایتم')
class Meta:
verbose_name = 'ایتم داخل پک'
verbose_name_plural = 'ایتم های داخل پک'
def __str__(self):
return self.item_title
class ProductModel(models.Model):
name = models.CharField(max_length=255, verbose_name='نام')
description = models.TextField(verbose_name='توضیحات')
image = models.ImageField(upload_to='product_main/', blank=True, null=True)
rating = models.PositiveIntegerField(default=0, verbose_name='امتیاز')
show = models.BooleanField(default=False, verbose_name='نمایش در خانه')
view = models.IntegerField(default=0, verbose_name='بازدید')
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True,
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
meta_description = models.CharField(
max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
meta_keywords = models.CharField(max_length=300, blank=True, null=True,
help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
meta_rating = models.FloatField(
default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
created_at = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت محصول')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL,
related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField(
'self', blank=True, verbose_name='محصولات مرتبط')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE,
related_name='products', verbose_name='فروشگاه', blank=True, null=True)
show_in_bot = models.BooleanField(
default=False, verbose_name='نمایش در ربات')
bot_banner = models.TextField(
null=True, blank=True, verbose_name='بنر ربات')
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class Meta:
verbose_name = 'محصول'
verbose_name_plural = 'محصولات'
ordering = ['category', 'name']
indexes = [
models.Index(fields=['slug'], name='product_slug_idx'),
models.Index(fields=['category'], name='product_category_idx'),
models.Index(fields=['name'], name='product_name_idx'),
models.Index(fields=['created_at'], name='product_created_at_idx'),
models.Index(fields=['show'], name='product_show_idx'),
models.Index(fields=['category', 'created_at'],
name='product_category_created_idx'),
models.Index(fields=['category', 'name'],
name='product_category_name_idx'),
]
class ProductDetailCategory(models.Model):
title = models.CharField(max_length=40, verbose_name='عنوان')
def __str__(self):
return self.title
class Meta:
verbose_name = 'دسته بندی جزيات'
verbose_name_plural = 'دسته بندی های جزيیات'
indexes = [
models.Index(fields=['title'], name='detail_category_title_idx'),
]
class CommentModel(models.Model):
product = models.ForeignKey(
ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
title = models.CharField(max_length=50)
content = models.TextField(verbose_name='محتوای نظر')
user = models.ForeignKey(
User, on_delete=models.CASCADE, verbose_name='کاربر')
timestamp = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت کامنت')
status_types = (
('reviewed_and_confirmed', 'بررسی و تایید شده'),
('reviewed_and_rejected', 'بررسی شده و رد شده'),
('not_reviwed', 'بررسی نشده'),
)
review_status = models.CharField(
default='not_reviwed', verbose_name='نشان دادن کامنت', max_length=30, choices=status_types)
class Meta:
verbose_name = 'نظر'
verbose_name_plural = 'نظرات'
indexes = [
models.Index(fields=['product'], name='comment_product_idx'),
models.Index(fields=['review_status'],
name='comment_review_status_idx'),
models.Index(fields=['product', 'review_status'],
name='comment_product_status_idx'),
models.Index(fields=['user'], name='comment_user_idx'),
models.Index(fields=['timestamp'], name='comment_timestamp_idx'),
]
def __str__(self):
return f"{self.user}-{self.content[:30]}"
class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
def __str__(self):
return self.name
class Meta:
verbose_name = 'نوع متغییر محصول'
verbose_name_plural = 'نوع های متغییر محصول'
class AttributeValue(models.Model):
attribute_type = models.ForeignKey(
AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(
verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
class Meta:
unique_together = ('attribute_type', 'value')
verbose_name = 'مقدار متغییر محصول'
verbose_name_plural = 'مقدار های متغییر محصول'
def __str__(self):
return f"{self.attribute_type}: {self.value}"
class ProductImageModel(models.Model):
name = models.CharField(max_length=30, verbose_name='نام عکس')
image = models.ImageField(upload_to='product_images/')
def __str__(self):
return self.name
class Meta:
verbose_name = 'عکس محصولات'
verbose_name_plural = 'عکس های محصولات'
class ProductDetailModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام جزيیات',
help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
detail_category = models.ForeignKey(
ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
class Meta:
verbose_name = 'جزیات محصول'
verbose_name_plural = 'جزیات محصول ها'
indexes = [
models.Index(fields=['detail_category'],
name='product_detail_category_idx'),
]
def __str__(self):
return f'جزيیات محصول {self.detail_category.title} - {self.name}'
class DetailModel(models.Model):
title = models.CharField(max_length=50, verbose_name='عنوان')
detail_text1 = models.CharField(max_length=150, verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(
max_length=150, verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text3 = models.CharField(
max_length=150, verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(
max_length=150, verbose_name='متن جزیات ۴', blank=True, null=True)
detail_model = models.ForeignKey(
ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details')
def __str__(self):
return f'{self.title}'
class Meta:
verbose_name = 'مدل جزیات'
verbose_name_plural = 'مدل های جزیات'
indexes = [
models.Index(fields=['detail_model'], name='detail_model_idx'),
models.Index(fields=['title'], name='detail_title_idx'),
]
class ProductVariant(DirtyFieldsMixin, models.Model):
product = models.ForeignKey(
ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
product_attributes = models.ManyToManyField(
AttributeValue, verbose_name='ویژگی‌ها', related_name='variant')
in_stock = models.PositiveIntegerField(
default=0, verbose_name='تعداد موجود')
price = models.PositiveBigIntegerField(
verbose_name='قیمت محاسبه شده', blank=True, null=True)
input_price = models.PositiveBigIntegerField(
default=0, verbose_name='قیمت محصول', help_text='قیمت ورودی محصول')
min_price = models.PositiveBigIntegerField(
verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
profit = models.PositiveBigIntegerField(
default=0, verbose_name='سود (تومان)', help_text='مقدار سود به ازای هر واحد به تومان')
special_discount_percent = models.SmallIntegerField(
default=0, verbose_name='درصد تخفیف ویژه', help_text='درصدی که از سود برای محاسبه تخفیف ویژه استفاده می‌شود')
currency_type = (
('dollor', 'با نوسان دلاری'),
('toman', 'بدون نوسان'),
)
in_pack_items = models.ManyToManyField(
InPackItems, blank=True, verbose_name='ایتم های داخل پک')
sell = models.IntegerField(default=0, verbose_name='فروش')
currency = models.CharField(
verbose_name='نوع نوسان', max_length=20, choices=currency_type)
price_in_dollor = models.DecimalField(
max_digits=12, decimal_places=5, blank=True, null=True,
verbose_name='قیمت به دلار', help_text='قیمت محصول به دلار (محاسبه خودکار)')
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
color = models.CharField(
verbose_name='رنگ', max_length=7, blank=True, null=True)
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
video = models.FileField(upload_to='product_videos/',
blank=True, null=True, verbose_name='ویدیو')
details = models.ManyToManyField(
ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
slider_category = models.ForeignKey(
ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت محصول')
class Meta:
verbose_name = 'تنوع محصول'
verbose_name_plural = 'تنوع‌های محصول'
indexes = [
models.Index(fields=['product', 'price', 'created_at'],
name='idx_product_price_created'),
models.Index(fields=['in_stock', 'discount'],
name='idx_stock_discount'),
models.Index(fields=['created_at'], name='idx_created'),
models.Index(fields=['price'], name='idx_price'),
models.Index(fields=['discount'],
name='product_variant_discount_idx'),
models.Index(fields=['in_stock'],
name='product_variant_in_stock_idx'),
models.Index(fields=['product'],
name='product_variant_product_idx'),
models.Index(fields=['product', 'in_stock'],
name='variant_product_stock_idx'),
models.Index(fields=['product', 'discount'],
name='variant_product_discount_idx'),
]
def __str__(self):
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
@property
def price_before_discount(self):
return self.price
@property
def price_after_discount(self):
return self.price - self.discount_amount
@property
def discount_amount(self):
return self.price * (self.discount / 100)
@property
def special_discount_amount_per_unit(self):
"""Calculate special discount amount per unit as profit * special_discount_percent / 100."""
try:
return int(self.profit * (self.special_discount_percent / 100))
except Exception:
return 0
def set_or_update_price(self, dollor_price=None):
if not dollor_price:
dollor_object, _ = DollorModel.objects.get_or_create(
unique_filed='unique')
dollor_price = dollor_object.price
if dollor_price is None:
raise ValidationError(
{"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
if self.currency == 'toman':
toman_price = self.input_price
# Clear dollar price when currency is toman
self.price_in_dollor = None
elif self.currency == 'dollor':
# Only recalculate dollar price if input_price has changed
if self.is_dirty(check_relationship=False) and 'input_price' in self.get_dirty_fields(check_relationship=False):
self.price_in_dollor = Decimal(str(self.input_price)) / Decimal(str(dollor_price))
# Always recalculate Toman price using stored dollar price and current rate
if self.price_in_dollor:
toman_price = self.price_in_dollor * Decimal(str(dollor_price))
else:
# Fallback if price_in_dollor is not set yet
toman_price = self.input_price
else:
toman_price = self.input_price
self.price_in_dollor = None
self.price = max(int(toman_price), self.min_price)
def save(self, *args, **kwargs):
self.set_or_update_price()
super().save(*args, **kwargs)