from django.utils.timezone import now, timedelta import uuid from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator from django.db import models from django.contrib.auth import get_user_model from django.db.models import Avg from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from autoslug import AutoSlugField # Create your models here. from core.models import ( AbstractStatusModel, AbstractDateTimeModel, ActiveOnlyManager, City, Currency) from products.models import Product # -----------------------------------------Client ------------------------------------------------------# class Client(AbstractStatusModel): def upload_file_to(self, filename): return "clients/{name}/{filename}".format(**{ 'name': self.name, 'filename': filename }) name = models.CharField(_('Название'), max_length=255) image = models.FileField(_('Изображение'), upload_to=upload_file_to) preview = models.FileField(_('Миниатюрка'), upload_to=upload_file_to) def __str__(self): return self.name class Meta: verbose_name = _('Клиент') verbose_name_plural = _('Клиенты') # -------------------------------------- Supply type dimensions --------------------------------------# SUPPLY_TYPE_HOUR_DIMENSION = 0 SUPPLY_TYPE_DAY_DIMENSION = 0 SUPPLY_TYPE_WEEK_DIMENSION = 0 SUPPLY_TYPE_MONTH_DIMENSION = 0 SUPPLY_TYPE_DIMENSION_CHOICES = ( (SUPPLY_TYPE_HOUR_DIMENSION, _('Час')), (SUPPLY_TYPE_DAY_DIMENSION, _('День')), (SUPPLY_TYPE_WEEK_DIMENSION, _('Неделя')), (SUPPLY_TYPE_MONTH_DIMENSION, _('Месяц')) ) SUPPLY_TYPE_DEFAULT_DIMENSION = SUPPLY_TYPE_HOUR_DIMENSION class SupplyType(AbstractDateTimeModel): name = models.CharField(_('Тип'), max_length=255) slug = AutoSlugField(populate_from='name', unique=True) min_term = models.IntegerField(_('от'), help_text=_('Минимальный срок поставки')) max_term = models.IntegerField(_('до'), help_text=_('Максимальный срок поставки')) term_dimension = models.SmallIntegerField( _('размерность'), choices=SUPPLY_TYPE_DIMENSION_CHOICES, default=SUPPLY_TYPE_DEFAULT_DIMENSION ) def get_formatted_desc(self): return _(" от {from} до {to} {dimension}".format(**{ 'from': self.min_term, 'to': self.max_term, 'dimension': SUPPLY_TYPE_DIMENSION_CHOICES[self.term_dimension][1] })) def __str__(self): return self.name class Meta: verbose_name = _('Тип поставки') verbose_name_plural = _('Тип поставки') class SupplyTarget(AbstractDateTimeModel): name = models.CharField(_('Назначение'), max_length=255) slug = AutoSlugField(populate_from='name', unique=True) status = models.PositiveSmallIntegerField(_('статус'), help_text=_('Необходимо указать числовой код статус')) def __str__(self): return self.name class Meta: verbose_name = _('Лицензия') verbose_name_plural = _('Лицензии') class Discount(AbstractDateTimeModel): def upload_file_to(self, filename): return "discounts/{name}/{filename}".format(**{ 'name': self.name, 'filename': filename }) name = models.CharField(_('Имя'), max_length=255) image = models.FileField(_('Изображение'), upload_to=upload_file_to, blank=True, null=True) code = models.CharField(_('Код'), max_length=50, blank=True, unique=True, default=str(uuid.uuid4())) valid_from = models.DateTimeField(_('Начало'), auto_now_add=True, blank=True) valid_to = models.DateTimeField(_('Конец'), default=now() + timedelta(days=7), blank=True) value = models.IntegerField(_('Процент'), validators=[MinValueValidator(0), MaxValueValidator(100)], default=0, help_text=_('Указываем целым числом. Пример: 30 = 30%')) active = models.BooleanField(_('Активная'), default=True) def __str__(self): return self.code class Meta: verbose_name = _('Дисконт') verbose_name_plural = _('Дисконт') # -------------------------------------- Offer status list -------------------------------------_# OFFER_STATUS_INACTIVE = 0 OFFER_STATUS_ACTIVE = 25 OFFER_STATUS_DELETED = 50 OFFER_STATUS_CHOICES = ( (OFFER_STATUS_ACTIVE, _('Активный')), (OFFER_STATUS_INACTIVE, _('Неактивный')), (OFFER_STATUS_DELETED, _('Удаленный')) ) OFFER_DEFAULT_CHOICE = OFFER_STATUS_INACTIVE class Offer(AbstractStatusModel): product = models.OneToOneField(Product, on_delete=models.CASCADE, primary_key=True, verbose_name=_('Продукт')) vendor_code = models.CharField(_('Артикул'), max_length=255, unique=True, help_text=_('Должен быть уникальным')) price = models.DecimalField(_('цена'), max_digits=10, decimal_places=2, validators=[MinValueValidator(1.00)], help_text=_('Цена за продукт')) currency = models.ForeignKey(Currency, verbose_name=_('Валюта'), on_delete=models.PROTECT, help_text=_('Цена по умолчанию в рублях')) supply_type = models.ForeignKey(SupplyType, verbose_name=_('Поставка'), on_delete=models.SET_NULL, blank=True, null=True) supply_target = models.ForeignKey(SupplyTarget, on_delete=models.SET_NULL, verbose_name=_('Лицензия'), blank=True, null=True, default=None) discount = models.ForeignKey(Discount, on_delete=models.SET_NULL, verbose_name=_('Дисконт'), blank=True, null=True) amount = models.IntegerField(_('Колличество'), default=1, validators=[MinValueValidator(1)]) cashback = models.DecimalField(_('Кешбек'), max_digits=6, decimal_places=2, default=0, help_text=_('Указаная сумма будет отображаться в выбранной валюте позиции')) note = models.TextField(_('Пометка'), blank=True, null=True) account_nds = models.BooleanField(_('с учетом НДС'), default=False) def get_price_with_discount(self): return self.price - ((self.discount.value * self.price) if self.discount else 0) def __str__(self): return self.product.name def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self.product: self.status = self.product.status super().save(force_insert, force_update, using, update_fields) class Meta: verbose_name = _('Позиция') verbose_name_plural = _('Позиции') STATUS_GAINED = 0 STATUS_SPENT = 100 CASHBACK_STATUS_CHOICES = ( (STATUS_GAINED, _('заработанный')), (STATUS_SPENT, _('потраченный')), ) STATUS_DEFAULT = STATUS_GAINED class CashBackManager(ActiveOnlyManager): def get_gained_cashback_sum(self, user): return self.get_queryset().filter(user=user, status=STATUS_GAINED).aggregate(Avg('amount')) def get_spent_cashback_sum(self, user): return self.get_queryset().filter(user=user, status=STATUS_SPENT).aggregate(Avg('amount')) class Cashback(AbstractDateTimeModel): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) amount = models.DecimalField(_('Сумма'), 'cashback', 7, 2, default=0) status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=CASHBACK_STATUS_CHOICES) objects = CashBackManager() @property def is_spent(self): return self.status == STATUS_SPENT class Meta: verbose_name_plural = _('cashback') verbose_name = _('cashback') ORDER_STATUS_NEW = 0 ORDER_STATUS_PENDING = 50 ORDER_STATUS_PAID = 100 ORDER_STATUS_CHOICES = ( (ORDER_STATUS_NEW, _('Новый')), (ORDER_STATUS_PENDING, _('Обрабатывается')), (ORDER_STATUS_PAID, _('Оплаченно')) ) ORDER_STATUS_DEFAULT = ORDER_STATUS_NEW class Order(AbstractStatusModel): order_code = models.CharField(_('код заказа'), max_length=255, default=str(uuid.uuid4())) customer_name = models.CharField(_('имя'), max_length=255) customer_email = models.EmailField(_('email'), blank=True, null=True, default=None) customer_user = models.ForeignKey( get_user_model(), on_delete=models.SET_NULL, verbose_name=_('пользователь'), blank=True, null=True ) phone_regex = RegexValidator( regex=r'^\((+7)|8)?\d{10}$', message="Phone number must be entered in the format: '+99999999999'. Up to 12 digits allowed." ) phone = models.CharField(_('телефон'), validators=[phone_regex], max_length=12) customer_address = models.TextField(_('адрес')) city = models.ForeignKey(City, on_delete=models.PROTECT, verbose_name=_('Город')) total_price = models.DecimalField(_('стоимость'), max_digits=10, decimal_places=2, default=0) comment = models.TextField(_('комментарий'), blank=True, null=True, default=None) status = models.SmallIntegerField(_('статус'), default=ORDER_STATUS_CHOICES, choices=ORDER_STATUS_DEFAULT) def __str__(self): return self.order_code class Meta: ordering = ('-create_at',) verbose_name = _('Заказ') verbose_name_plural = _('Заказы') # ------------------------------------------ Buying status --------------------------------------------------- # BUYING_STATUS_IN_CART = 25 BUYING_STATUS_PENDING = 50 BUYING_STATUS_PAID = 75 BUYING_STATUS_CHOICES = ( (BUYING_STATUS_IN_CART, _('В корзине')), (BUYING_STATUS_PENDING, _('Обрабатываеться')), (BUYING_STATUS_PAID, _('Оплаченно')) ) BUYING_DEFAULT_CHOICE = BUYING_STATUS_IN_CART class BuyingManager(ActiveOnlyManager, models.Manager): def get_user_buyings(self, user): qs = self.get_queryset() return qs.filter(user=user).all() def get_buying_total_price(self, user=None): qs = self.get_user_buyings(user) if user else self.get_queryset() return qs.aggregate(Sum('total_price')) def get_buying_total_bonus_points(self, user=None): qs = self.get_user_buyings(user) if user else self.get_queryset() return qs.aggregate(Sum('bonus_points')) def get_buying_total_cashback(self, user=None): qs = self.get_user_buyings(user) if user else self.get_queryset() return qs.select_related('buying_cashback').aggregate(Sum('amount')) class Buying(AbstractStatusModel): order = models.ForeignKey(Order, verbose_name=_('пользователь'), on_delete=models.CASCADE) user = models.ForeignKey(get_user_model(), verbose_name=_('пользователь'), on_delete=models.CASCADE) offer = models.ForeignKey(Offer, verbose_name=_('позиция'), on_delete=models.CASCADE) bonus_points = models.IntegerField(_('бонусы'), validators=(MinValueValidator(0),)) status = models.SmallIntegerField(_('статус'), default=BUYING_DEFAULT_CHOICE, choices=BUYING_STATUS_CHOICES) amount = models.SmallIntegerField(_('колличество'), default=0) total_price = models.DecimalField(_('цена'), max_digits=10, decimal_places=2) active = BuyingManager() @property def is_in_cart(self): return self.status == BUYING_STATUS_IN_CART @property def is_pending(self): return self.status == BUYING_STATUS_PENDING @property def is_paid(self): return self.status == BUYING_STATUS_PAID def __str__(self): return "{product_name}({product_amount}) - {price}".format(**{ 'product_name': self.offer.product.name, 'product_amount': self.amount, 'price': self.total_price }) class Meta: verbose_name = _('Покупка') verbose_name_plural = _('Покупки') @receiver(post_save, sender=Order) def product_in_order_post_save(sender, instance, created, **kwargs): order = instance.order all_products_in_order = Buying.objects.filter(order=instance, status=ORDER_STATUS_NEW) order_total_price = sum(item.total_price for item in all_products_in_order) # if order.discount: # order.total_price = order_total_price * (order.discount_value / Decimal('100')) if order.points_quant: order.total_price = order_total_price - order.points_quant else: order.total_price = order_total_price order.save(force_update=True)