You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
13 KiB
339 lines
13 KiB
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)
|
|
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(_('статус'), choices=ORDER_STATUS_CHOICES, default=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)
|
|
|