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.
510 lines
23 KiB
510 lines
23 KiB
from datetime import timedelta, date, datetime
|
|
from decimal import Decimal
|
|
|
|
import arrow
|
|
import short_url
|
|
from django.db.models.functions import Coalesce
|
|
|
|
from paymentwall import Pingback
|
|
from polymorphic.models import PolymorphicModel
|
|
from polymorphic.managers import PolymorphicManager
|
|
|
|
from django.db.models import Func, F, Q, Max
|
|
from django.db import models
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.postgres.fields import ArrayField, JSONField
|
|
from django.core.validators import RegexValidator, MaxValueValidator
|
|
from django.utils.timezone import now
|
|
from django.conf import settings
|
|
|
|
from apps.content.models import Package
|
|
from project.utils import weekdays_in_date_range
|
|
|
|
from apps.course.models import Course
|
|
from apps.config.models import Config
|
|
from apps.school.models import SchoolSchedule
|
|
from apps.notification.utils import send_email
|
|
from project.utils import dates_overlap
|
|
|
|
|
|
config = Config.load()
|
|
User = get_user_model()
|
|
|
|
CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}' \
|
|
r'|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$'
|
|
|
|
|
|
class AuthorBalance(models.Model):
|
|
IN = 0
|
|
OUT = 1
|
|
TYPE_CHOICES = (
|
|
(IN, 'In'),
|
|
(OUT, 'Out'),
|
|
)
|
|
PENDING = 0
|
|
ACCEPTED = 1
|
|
DECLINED = 2
|
|
STATUS_CHOICES = (
|
|
(PENDING, 'Pending'),
|
|
(ACCEPTED, 'Accepted'),
|
|
(DECLINED, 'Declined'),
|
|
)
|
|
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Автор',
|
|
null=True, blank=True, related_name='balances')
|
|
type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=0)
|
|
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0)
|
|
commission = models.DecimalField('Комиссия', max_digits=8, decimal_places=2, default=0)
|
|
status = models.PositiveSmallIntegerField('Статус', choices=STATUS_CHOICES, default=0)
|
|
payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж')
|
|
cause = models.TextField('Причина отказа', null=True, blank=True)
|
|
card = models.CharField(max_length=20, validators=[RegexValidator(regex=CREDIT_CARD_RE)], null=True, blank=True)
|
|
|
|
declined_send_at = models.DateTimeField(null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
update_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Баланс'
|
|
verbose_name_plural = 'Балансы'
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.type == self.IN and not self.id:
|
|
self.commission = self.calc_commission()
|
|
if self.type == self.OUT:
|
|
if self.status == self.DECLINED and not self.declined_send_at:
|
|
send_email(
|
|
'Отказ вывода средств',
|
|
self.author.email,
|
|
'notification/email/decline_withdrawal.html',
|
|
author_balance=self,
|
|
)
|
|
self.declined_send_at = now()
|
|
super().save(*args, **kwargs)
|
|
|
|
def calc_commission(self):
|
|
return self.amount * config.SERVICE_COMMISSION / 100
|
|
|
|
|
|
class PaymentManger(PolymorphicManager):
|
|
|
|
def all(self):
|
|
return self.filter(status__isnull=False)
|
|
|
|
def paid(self):
|
|
return self.filter(status__in=Payment.PW_PAID_STATUSES)
|
|
|
|
|
|
class Payment(PolymorphicModel):
|
|
PW_PAID_STATUSES = [
|
|
Pingback.PINGBACK_TYPE_REGULAR,
|
|
Pingback.PINGBACK_TYPE_GOODWILL,
|
|
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
|
|
]
|
|
PW_STATUS_CHOICES = (
|
|
(Pingback.PINGBACK_TYPE_REGULAR, 'regular',),
|
|
(Pingback.PINGBACK_TYPE_GOODWILL, 'goodwill',),
|
|
(Pingback.PINGBACK_TYPE_NEGATIVE, 'negative',),
|
|
(Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, 'risk under review',),
|
|
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, 'risk reviewed accepted',),
|
|
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, 'risk reviewed declined',),
|
|
(Pingback.PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED, 'risk authorization voided',),
|
|
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION, 'subscription cancelation',),
|
|
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_EXPIRED, 'subscription expired',),
|
|
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED, 'subscription payment failed',),
|
|
)
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='payments')
|
|
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False)
|
|
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True)
|
|
data = JSONField('Данные платежа от провайдера', default={}, editable=False)
|
|
roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
update_at = models.DateTimeField(auto_now=True)
|
|
bonus = models.ForeignKey('payment.UserBonus', blank=True, null=True, on_delete=models.SET_NULL,
|
|
related_name='purchase_payments')
|
|
|
|
objects = PaymentManger()
|
|
|
|
class Meta:
|
|
verbose_name = 'Платеж'
|
|
verbose_name_plural = 'Платежи'
|
|
ordering = ('created_at',)
|
|
|
|
@classmethod
|
|
def adjust_date_bounds(cls, date_start=None, date_end=None, is_camp=False):
|
|
school_start = date((date_start or date_end).year, 9, 1)
|
|
school_end = date((date_end or date_start).year, 5, 31)
|
|
camp_start = date((date_start or date_end).year, 6, 1)
|
|
camp_end = date((date_end or date_start).year, 8, 31)
|
|
|
|
if date_start:
|
|
if is_camp:
|
|
if date_start < camp_start:
|
|
date_start = camp_start
|
|
elif school_end < date_start < school_start:
|
|
date_start = school_start
|
|
|
|
if date_end:
|
|
if is_camp:
|
|
if date_end > camp_end:
|
|
date_end = camp_end
|
|
elif school_end < date_end < school_start:
|
|
date_end = school_end
|
|
|
|
if date_start and date_end:
|
|
return [date_start, date_end]
|
|
else:
|
|
return date_start or date_end
|
|
|
|
@classmethod
|
|
def date_add(cls, date_start, days=0, months=0, is_camp=False):
|
|
date_end = arrow.get(date_start + timedelta(days=days), settings.TIME_ZONE).shift(
|
|
months=months).date() - timedelta(1)
|
|
if months == 1:
|
|
if is_camp or (date_start.month == 2 and date_start.day >= 28) or (
|
|
date_start.day == 31 and date_end.day <= 30) \
|
|
or (date_start.month == 1 and date_start.day >= 29 and date_end.day == 28):
|
|
date_end = date_start.replace(day=1, month=date_start.month + 1) - timedelta(1)
|
|
return date_end
|
|
|
|
@classmethod
|
|
def get_date_range(cls, date_start=None, days=0, months=0, is_camp=False):
|
|
date_start = date_start or now().date()
|
|
if isinstance(date_start, datetime):
|
|
date_start = date_start.date()
|
|
date_start = cls.adjust_date_bounds(date_start=date_start, is_camp=is_camp)
|
|
if is_camp and date_start.month == 6 and date_start.day > 16:
|
|
date_start = date_start.replace(month=7, day=1)
|
|
date_end = cls.date_add(date_start, days, months, is_camp)
|
|
if is_camp:
|
|
date_end = cls.adjust_date_bounds(date_end, is_camp=is_camp)
|
|
return [date_start, date_end]
|
|
|
|
@classmethod
|
|
def calc_amount(cls, package=None, payment=None, user=None, course=None, date_start=None, weekdays=None, is_camp=False):
|
|
price = 0
|
|
discount = 0
|
|
referral_bonus = 0
|
|
referrer_bonus = 0
|
|
if isinstance(payment, CoursePayment):
|
|
course = payment.course
|
|
user = payment.user
|
|
if isinstance(payment, SchoolPayment):
|
|
user = payment.user
|
|
date_start = payment.date_start
|
|
package=payment.package
|
|
if isinstance(payment, DrawingCampPayment):
|
|
user = payment.user
|
|
date_start = payment.date_start
|
|
if issubclass(cls, DrawingCampPayment):
|
|
is_camp = True
|
|
date_start, date_end = Payment.get_date_range(date_start, months=package.duration if package else 1, is_camp=is_camp)
|
|
if hasattr(user, 'referral') and not user.referral.payment:
|
|
referral_bonus = user.referral.bonus
|
|
referrer_bonus = user.referral.referrer_bonus
|
|
if payment and payment.is_paid():
|
|
price = payment.amount
|
|
elif isinstance(payment, GiftCertificatePayment):
|
|
price = payment.gift_certificate.price
|
|
elif course:
|
|
paid_before = CoursePayment.objects.filter(user=user, course=course, status__in=Payment.PW_PAID_STATUSES).exists()
|
|
price = course.price / 2 if paid_before else course.price
|
|
elif is_camp:
|
|
if date_start.day == 1:
|
|
price = DrawingCampPayment.MONTH_PRICE
|
|
else:
|
|
weekdays_count = weekdays_in_date_range(date_start, date_end)
|
|
weekdays_count = sum(weekdays_count[wd] for wd in DrawingCampPayment.WEEKDAYS)
|
|
all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
|
|
all_weekdays_count = sum(all_weekdays_count[wd] for wd in DrawingCampPayment.WEEKDAYS)
|
|
price = round(DrawingCampPayment.MONTH_PRICE / all_weekdays_count * weekdays_count)
|
|
else:
|
|
if user:
|
|
school_payments = SchoolPayment.objects.filter(
|
|
user=user,
|
|
date_start__lte=date_start,
|
|
date_end__gte=date_start,
|
|
status__in=Payment.PW_PAID_STATUSES,
|
|
)
|
|
prev_school_payment = school_payments.filter(add_days=False).last()
|
|
if prev_school_payment:
|
|
date_start, date_end = Payment.get_date_range(prev_school_payment.date_end + timedelta(1),
|
|
months=package.duration, is_camp=False)
|
|
school_schedules = SchoolSchedule.objects.filter(is_camp=False).exclude(title='')
|
|
weekdays = list(school_schedules.values_list('weekday', flat=True))
|
|
# FIXME после мая 2019 убрать?
|
|
# Если хотят купить школу в мае, то оплатить ее можно только до 31 мая, потом школа закроется
|
|
if date_start.month == 5:
|
|
weekdays_count = weekdays_in_date_range(date_start, date_end)
|
|
weekdays_count = sum(weekdays_count[wd] for wd in weekdays)
|
|
all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
|
|
all_weekdays_count = sum(all_weekdays_count[wd] for wd in weekdays)
|
|
price = package.price // all_weekdays_count * weekdays_count
|
|
else:
|
|
price = package.price
|
|
amount = price - discount
|
|
referral_bonus = round(amount * referral_bonus / 100)
|
|
referrer_bonus = round(amount * referrer_bonus / 100)
|
|
return {
|
|
'price': price,
|
|
'amount': amount,
|
|
'referral_bonus': referral_bonus,
|
|
'referrer_bonus': referrer_bonus,
|
|
'discount': discount,
|
|
'date_start': date_start,
|
|
'date_end': date_end,
|
|
'weekdays': weekdays,
|
|
}
|
|
|
|
# TODO? change to property?
|
|
def calc_commission(self):
|
|
return self.amount * config.SERVICE_COMMISSION / 100
|
|
|
|
# TODO change to property
|
|
def is_paid(self):
|
|
return self.status in self.PW_PAID_STATUSES
|
|
|
|
# TODO? delete ? change to property
|
|
def is_deliverable(self):
|
|
return self.status in [
|
|
Pingback.PINGBACK_TYPE_REGULAR,
|
|
Pingback.PINGBACK_TYPE_GOODWILL,
|
|
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
|
|
]
|
|
|
|
# TODO change to property
|
|
def is_under_review(self):
|
|
return self.status == Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW
|
|
|
|
# TODO change to property
|
|
def is_cancelable(self):
|
|
return self.status in [
|
|
Pingback.PINGBACK_TYPE_NEGATIVE,
|
|
Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED,
|
|
]
|
|
|
|
def save(self, *args, **kwargs):
|
|
amount_data = type(self).calc_amount(payment=self)
|
|
if not self.is_paid():
|
|
if not self.bonus:
|
|
self.amount = amount_data.get('amount')
|
|
if isinstance(self, SchoolPayment):
|
|
self.weekdays = amount_data.get('weekdays')
|
|
super().save(*args, **kwargs)
|
|
if self.is_paid():
|
|
if isinstance(self, CoursePayment):
|
|
if not getattr(self, 'authorbalance', None):
|
|
AuthorBalance.objects.create(
|
|
author=self.course.author,
|
|
amount=self.amount,
|
|
payment=self,
|
|
)
|
|
if isinstance(self, GiftCertificatePayment):
|
|
ugs, created = UserGiftCertificate.objects.get_or_create(user=self.user, gift_certificate=self.gift_certificate,
|
|
payment=self)
|
|
if created:
|
|
from apps.notification.tasks import send_gift_certificate
|
|
send_gift_certificate(ugs.id)
|
|
# Если это не первая покупка, - отправляем бонусы юзеру
|
|
if self.user.paid_one_more and not self.user.bonuses.filter(
|
|
is_service=True, action_name=UserBonus.ACTION_PAID_ONE_MORE).count():
|
|
UserBonus.objects.create(user=self.user, amount=UserBonus.AMOUNT_PAID_ONE_MORE, is_service=True,
|
|
action_name=UserBonus.ACTION_PAID_ONE_MORE)
|
|
# Если юзер реферал и нет платежа, где применялась скидка
|
|
# (после первой покупки реферала начисляются бонусы ему и пригласившему рефереру)
|
|
if hasattr(self.user, 'referral') and not self.user.referral.payment:
|
|
# Платеж - как сигнал, что была покупка и бонусы отправлены
|
|
self.user.referral.payment = self
|
|
self.user.referral.save()
|
|
# Отправляем кэшбэк
|
|
self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus'))
|
|
|
|
|
|
class CoursePayment(Payment):
|
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments')
|
|
access_expire = models.DateField('Доступ к курсу до даты', null=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Платеж за курс'
|
|
verbose_name_plural = 'Платежи за курсы'
|
|
|
|
|
|
class SchoolPayment(Payment):
|
|
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')
|
|
add_days = models.BooleanField('Докупленные дни', default=False)
|
|
package = models.ForeignKey(Package, null=True, blank=True, on_delete=models.SET_NULL)
|
|
date_start = models.DateField('Дата начала подписки', null=True, blank=True)
|
|
date_end = models.DateField('Дата окончания подписки', null=True, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Платеж за школу'
|
|
verbose_name_plural = 'Платежи за школу'
|
|
|
|
def __str__(self):
|
|
days = ', '.join([
|
|
dict(SchoolSchedule.WEEKDAY_CHOICES).get(weekday, '')
|
|
for weekday in sorted(self.weekdays)
|
|
])
|
|
return days
|
|
|
|
@property
|
|
def date_end_humanize(self):
|
|
return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru')
|
|
|
|
|
|
class DrawingCampPayment(Payment):
|
|
MONTH_PRICE = Decimal(1990)
|
|
WEEKDAYS = {1, 3, 5}
|
|
|
|
date_start = models.DateField('Дата начала подписки', null=True, blank=True)
|
|
date_end = models.DateField('Дата окончания подписки', null=True, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Платеж за рисовальный лагерь'
|
|
verbose_name_plural = 'Платежи за рисовальный лагерь'
|
|
|
|
def __str__(self):
|
|
return '%s - %s' % (self.date_start.strftime('%d/%m/%Y'), self.date_end.strftime('%d/%m/%Y'))
|
|
|
|
@property
|
|
def date_end_humanize(self):
|
|
return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru')
|
|
|
|
|
|
class GiftCertificatePayment(Payment):
|
|
gift_certificate = models.ForeignKey('GiftCertificate', on_delete=models.CASCADE,
|
|
verbose_name='Подарочный сертификат', related_name='payments')
|
|
|
|
class Meta:
|
|
verbose_name = 'Платеж за подарочный сертификат'
|
|
verbose_name_plural = 'Платежи за подарочные сертификаты'
|
|
|
|
|
|
class UserBonus(models.Model):
|
|
ACTION_FILL_PROFILE = 'fill_profile'
|
|
ACTION_PAID_ONE_MORE = 'paid_one_more'
|
|
ACTION_HAVE_REVIEW = 'have_review'
|
|
ACTION_CHILD_BIRTHDAY = 'child_birthday'
|
|
AMOUNT_FILL_PROFILE = 50
|
|
AMOUNT_PAID_ONE_MORE = 100
|
|
AMOUNT_HAVE_REVIEW = 300
|
|
AMOUNT_CHILD_BIRTHDAY = 200
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bonuses')
|
|
amount = models.DecimalField(max_digits=8, decimal_places=2, default=0)
|
|
payment = models.ForeignKey(Payment, on_delete=models.SET_NULL, null=True, blank=True)
|
|
referral = models.ForeignKey('user.Referral', on_delete=models.SET_NULL, null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
is_service = models.BooleanField(default=False)
|
|
action_name = models.CharField(max_length=20, blank=True, default='')
|
|
notified_at = models.DateTimeField(blank=True, null=True)
|
|
|
|
class Meta:
|
|
ordering = ('created_at',)
|
|
|
|
def __str__(self):
|
|
return '%s %s' % (self.user_id, self.amount)
|
|
|
|
|
|
class GiftCertificate(models.Model):
|
|
price = models.DecimalField(max_digits=8, decimal_places=2, default=0)
|
|
cover = models.CharField(max_length=255, blank=True, default='')
|
|
|
|
class Meta:
|
|
ordering = ('price',)
|
|
|
|
|
|
class UserGiftCertificate(models.Model):
|
|
gift_certificate = models.ForeignKey(GiftCertificate, on_delete=models.CASCADE,)
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gift_certificates')
|
|
payment = models.ForeignKey(Payment, on_delete=models.SET_NULL, null=True)
|
|
bonuses_sent = models.ForeignKey(UserBonus, on_delete=models.CASCADE, blank=True, null=True)
|
|
|
|
@property
|
|
def code(self):
|
|
return short_url.encode_url(self.id) if self.id else None
|
|
|
|
|
|
class DiscountManager(models.Manager):
|
|
|
|
def get_actual_discounts(self, user=None):
|
|
now_dt = now()
|
|
qs = self.filter(start__lte=now_dt, end__gte=now_dt, is_active=True, promo_id__isnull=True)
|
|
if user:
|
|
qs = qs.filter(Q(client=user) | Q(client__isnull=True))
|
|
else:
|
|
qs = qs.filter(client__isnull=True)
|
|
return qs
|
|
|
|
def get_courses_discounts(self, user=None, course=None):
|
|
actual_discounts = self.get_actual_discounts(user=user)
|
|
all_courses_max_discount = actual_discounts.filter(product=Discount.PRODUCT_ALL_COURSES)\
|
|
.aggregate(max_discount=Max('value'))
|
|
if course:
|
|
actual_discounts = actual_discounts.filter(course=course)
|
|
courses_max_discounts = actual_discounts.filter(product=Discount.PRODUCT_ONE_COURSE).values('course')\
|
|
.annotate(max_discount=Max('value'))
|
|
return all_courses_max_discount, courses_max_discounts
|
|
|
|
def get_packages_discounts(self, user=None, package=None):
|
|
actual_discounts = self.get_actual_discounts(user=user)
|
|
all_packages_max_discount = actual_discounts.filter(product=Discount.PRODUCT_ALL_PACKAGES) \
|
|
.aggregate(max_discount=Max('value'))
|
|
if package:
|
|
actual_discounts = actual_discounts.filter(package=package)
|
|
packages_max_discounts = actual_discounts.filter(product=Discount.PRODUCT_ONE_PACKAGE).values('package') \
|
|
.annotate(max_discount=Max('value'))
|
|
return all_packages_max_discount, packages_max_discounts
|
|
|
|
|
|
class Discount(models.Model):
|
|
PRODUCT_ALL_COURSES = 0
|
|
PRODUCT_ALL_PACKAGES = 1
|
|
PRODUCT_ONE_COURSE = 2
|
|
PRODUCT_ONE_PACKAGE = 3
|
|
|
|
USAGE_TYPE_LIMIT = 0
|
|
USAGE_TYPE_UNLIMITED = 1
|
|
|
|
PRODUCT_CHOICES = (
|
|
(PRODUCT_ALL_COURSES, 'Все курсы'),
|
|
(PRODUCT_ALL_PACKAGES, 'Все подписки'),
|
|
(PRODUCT_ONE_COURSE, 'Один курс'),
|
|
(PRODUCT_ONE_PACKAGE, 'Одна подписка')
|
|
)
|
|
|
|
USAGE_TYPE_CHOICES = (
|
|
(USAGE_TYPE_LIMIT, 'Ограничен по количеству использований'),
|
|
(USAGE_TYPE_UNLIMITED, 'Не ограничен по количеству использований')
|
|
)
|
|
|
|
name = models.CharField(verbose_name='Название', max_length=200)
|
|
client = models.ForeignKey(User, on_delete=models.CASCADE, related_name='discounts', null=True, blank=True,
|
|
verbose_name='Пользователь скидки')
|
|
product = models.PositiveSmallIntegerField('На какие продукты распространяется скидка', choices=PRODUCT_CHOICES,
|
|
default=PRODUCT_ALL_COURSES)
|
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_discounts', null=True, blank=True,
|
|
verbose_name='Курс на который распространяется скидка')
|
|
package = models.ForeignKey(Package, verbose_name='Подписка на которую распространяется скидка',
|
|
on_delete=models.CASCADE, related_name='package_discounts', null=True, blank=True)
|
|
promo_id = models.CharField(verbose_name='Промокод', max_length=20, null=True, blank=True)
|
|
usage_type = models.PositiveSmallIntegerField(verbose_name='Тип использования',
|
|
choices=USAGE_TYPE_CHOICES, default=USAGE_TYPE_LIMIT)
|
|
usage_count_limit = models.PositiveIntegerField(verbose_name='Сколько раз можно использовать', null=True,
|
|
blank=True)
|
|
activate_count = models.PositiveIntegerField(verbose_name='Сколько раз использован', default=0)
|
|
value = models.PositiveIntegerField(verbose_name='Размер скидки в %', default=0,
|
|
validators=[MaxValueValidator(100, 'Размер скидки не может быть больше 100')])
|
|
start = models.DateTimeField(verbose_name='Дата начала действия скидки', default=now)
|
|
end = models.DateTimeField(verbose_name='Дата окончания скидки')
|
|
is_active = models.BooleanField(verbose_name='Активность', default=True)
|
|
author = models.ForeignKey(User, verbose_name='Автор', null=True, blank=True, related_name='author_discounts',
|
|
on_delete=models.CASCADE)
|
|
|
|
objects = DiscountManager()
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Скидка'
|
|
verbose_name_plural = 'Скидки'
|
|
|