from decimal import Decimal import random import string from io import BytesIO from threading import local as LocalContext from django.conf import settings from django.contrib.admin import display from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, UserManager as _UserManager from django.core.files.base import ContentFile from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from django.db.models import F, Case, When, DecimalField, Prefetch from django.db.models.lookups import GreaterThan from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_cleanup import cleanup from store.utils import create_preview, concat_not_null_values class GlobalSettings(models.Model): context = LocalContext() # currency yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0) # Chinadelivery delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0) commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0) pickup_address = models.CharField('Адрес пункта самовывоза', max_length=200, blank=True, null=True) class Meta: verbose_name = 'Глобальные настройки' verbose_name_plural = 'Глобальные настройки' def save(self, *args, **kwargs): # Store only one instance of GlobalSettings self.__class__.objects.exclude(id=self.id).delete() super().save(*args, **kwargs) GlobalSettings.context.cached = self def __str__(self) -> str: return f'GlobalSettings for {self.id}' @classmethod def load(cls) -> 'GlobalSettings': if hasattr(cls.context, 'cached'): return cls.context.cached obj, _ = cls.objects.get_or_create(id=1) cls.context.cached = obj return obj class Category(models.Model): name = models.CharField('Название', max_length=20) slug = models.SlugField('Идентификатор', unique=True) delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0) # Chinadelivery2 def __str__(self): return self.name class Meta: verbose_name = 'Категория' verbose_name_plural = 'Категории' class UserQuerySet(models.QuerySet): pass class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager): def _create_user(self, email, password, **extra_fields): if not email: raise ValueError("The given email must be set") email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user def create_user(self, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("job_title", User.ADMIN) if extra_fields.get("is_staff") is not True: raise ValueError("Superuser must have is_staff=True.") return self._create_user(email, password, **extra_fields) class User(AbstractUser): ADMIN = "admin" ORDER_MANAGER = "ordermanager" PRODUCT_MANAGER = "productmanager" JOB_CHOICES = ( (ADMIN, 'Администратор'), (ORDER_MANAGER, 'Менеджер по заказам'), (PRODUCT_MANAGER, 'Менеджер по закупкам'), ) # Login by email USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] username = None email = models.EmailField("Эл. почта", unique=True) first_name = models.CharField(_("first name"), max_length=150, blank=True, null=True) last_name = models.CharField(_("last name"), max_length=150, blank=True, null=True) middle_name = models.CharField("Отчество", max_length=150, blank=True, null=True) job_title = models.CharField("Должность", max_length=30, choices=JOB_CHOICES) objects = UserManager() @property def is_superuser(self): return self.job_title == self.ADMIN @display(description='ФИО') def full_name(self): return concat_not_null_values(self.last_name, self.first_name, self.middle_name) class PromocodeQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True) class Promocode(models.Model): name = models.CharField('Название', max_length=100, unique=True) discount = models.DecimalField('Скидка', max_digits=10, decimal_places=2, validators=[MinValueValidator(0), MaxValueValidator(100)]) free_delivery = models.BooleanField('Бесплатная доставка', default=False) # freedelivery no_comission = models.BooleanField('Без комиссии', default=False) # nocomission is_active = models.BooleanField('Активен', default=True) objects = PromocodeQuerySet.as_manager() def __str__(self): return self.name class Meta: verbose_name = 'Промокод' verbose_name_plural = 'Промокоды' class PaymentMethod(models.Model): name = models.CharField('Название', max_length=30) slug = models.SlugField('Идентификатор', unique=True) cardnumber = models.CharField('Номер карты', max_length=30) requisites = models.CharField('Реквизиты', max_length=200) class Meta: verbose_name = 'Метод оплаты' verbose_name_plural = 'Методы оплаты' def __str__(self): return self.name @cleanup.select class Image(models.Model): image = models.ImageField(upload_to='checklist_images') is_preview = models.BooleanField(default=False) checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE, related_name='images') class Meta: verbose_name = 'Изображение' verbose_name_plural = 'Изображения' def __str__(self): return getattr(self.image, 'name') def generate_checklist_id(): """ Generate unique id for Checklist """ all_ids = Checklist.objects.all().values_list('id', flat=True) allowed_chars = string.ascii_letters + string.digits while True: generated_id = ''.join(random.choice(allowed_chars) for _ in range(settings.CHECKLIST_ID_LENGTH)) if generated_id not in all_ids: return generated_id class ChecklistQuerySet(models.QuerySet): def with_base_related(self): return self.select_related('manager', 'category', 'payment_method', 'promocode')\ .prefetch_related(Prefetch('images', to_attr='_images')) def default_ordering(self): return self.order_by(F('status_updated_at').desc(nulls_last=True)) def annotate_price_rub(self): yuan_rate = GlobalSettings.load().yuan_rate return self.annotate(_price_rub=F('price_yuan') * yuan_rate) def annotate_commission_rub(self): commission = GlobalSettings.load().commission_rub return self.annotate(_commission_rub=Case( When(GreaterThan(F("_price_rub"), 150_000), then=F("_price_rub") * settings.COMMISSION_OVER_150K), default=commission, output_field=DecimalField() )) @cleanup.select class Checklist(models.Model): # Statuses class Status: DRAFT = "draft" NEW = "neworder" PAYMENT = "payment" BUYING = "buying" BOUGHT = "bought" CHINA = "china" CHINA_RUSSIA = "chinarush" RUSSIA = "rush" SPLIT_WAITING = "split_waiting" SPLIT_PAID = "split_paid" CDEK = "cdek" COMPLETED = "completed" PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED) CHOICES = ( (DRAFT, 'Черновик'), (NEW, 'Новый заказ'), (PAYMENT, 'Проверка оплаты'), (BUYING, 'На закупке'), (BOUGHT, 'Закуплен'), (CHINA, 'На складе в Китае'), (CHINA_RUSSIA, 'Доставка на склад РФ'), (RUSSIA, 'На складе в РФ'), (SPLIT_WAITING, 'Сплит: ожидание оплаты 2й части'), (SPLIT_PAID, 'Сплит: полностью оплачено'), (CDEK, 'Доставляется СДЭК'), (COMPLETED, 'Завершен'), ) # Delivery class DeliveryType: PICKUP = "pickup" CDEK = "cdek" CHOICES = ( (PICKUP, 'Самовывоз из шоурума'), (CDEK, 'Пункт выдачи заказов CDEK'), ) created_at = models.DateTimeField(auto_now_add=True) status_updated_at = models.DateTimeField('Дата обновления статуса заказа', auto_now_add=True) id = models.CharField(primary_key=True, max_length=settings.CHECKLIST_ID_LENGTH, default=generate_checklist_id, editable=False) status = models.CharField('Статус заказа', max_length=15, choices=Status.CHOICES, default=Status.NEW) # managerid manager = models.ForeignKey('User', verbose_name='Менеджер', on_delete=models.SET_NULL, blank=True, null=True) product_link = models.URLField('Ссылка на товар', null=True, blank=True) category = models.ForeignKey('Category', verbose_name="Категория", blank=True, null=True, on_delete=models.SET_NULL) subcategory = models.CharField('Подкатегория', max_length=20, blank=True, null=True) brand = models.CharField('Бренд', max_length=100, null=True, blank=True) model = models.CharField('Модель', max_length=100, null=True, blank=True) size = models.CharField('Размер', max_length=30, null=True, blank=True) # curencycurency2 price_yuan = models.DecimalField('Цена в юанях', max_digits=10, decimal_places=2, default=0) # TODO: replace real_price by parser real_price = models.DecimalField('Реальная цена', max_digits=10, decimal_places=2, null=True, blank=True) # promo promocode = models.ForeignKey('Promocode', verbose_name='Промокод', on_delete=models.PROTECT, null=True, blank=True) comment = models.CharField('Комментарий', max_length=200, null=True, blank=True) # buyername buyer_name = models.CharField('Имя покупателя', max_length=100, null=True, blank=True) # buyerphone buyer_phone = models.CharField('Телефон покупателя', max_length=100, null=True, blank=True) # tg buyer_telegram = models.CharField('Telegram покупателя', max_length=100, null=True, blank=True) # receivername receiver_name = models.CharField('Имя получателя', max_length=100, null=True, blank=True) # reveiverphone receiver_phone = models.CharField('Телефон получателя', max_length=100, null=True, blank=True) is_split_payment = models.BooleanField('Оплата частями', default=False) # paymenttype payment_method = models.ForeignKey('PaymentMethod', verbose_name='Метод оплаты', null=True, blank=True, on_delete=models.SET_NULL) payment_proof = models.ImageField('Подтверждение оплаты', upload_to='docs', null=True, blank=True) # paymentproovement receipt = models.ImageField('Фото чека', upload_to='docs', null=True, blank=True) # checkphoto delivery = models.CharField('Тип доставки', max_length=10, choices=DeliveryType.CHOICES, null=True, blank=True) # trackid poizon_tracking = models.CharField('Трек-номер Poizon', max_length=100, null=True, blank=True) cdek_tracking = models.CharField('Трек-номер СДЭК', max_length=100, null=True, blank=True) cdek_barcode_pdf = models.FileField('Штрих-код СДЭК в PDF', upload_to='docs', null=True, blank=True) objects = ChecklistQuerySet.as_manager() class Meta: verbose_name = 'Заказ' verbose_name_plural = 'Заказы' @property def price_rub(self) -> Decimal: # Prefer annotated field if hasattr(self, '_price_rub'): return self._price_rub return GlobalSettings.load().yuan_rate * self.price_yuan @property def full_price(self) -> Decimal: price = self.price_rub free_delivery = False no_comission = False if self.promocode_id: promocode = self.promocode price -= promocode.discount * self.price_rub free_delivery = promocode.free_delivery no_comission = promocode.no_comission if not free_delivery: price += GlobalSettings.load().delivery_price_CN + self.delivery_price_CN_RU if not no_comission: price += self.commission_rub return price @property def delivery_price_CN_RU(self) -> Decimal: return getattr(self.category, 'delivery_price_CN_RU', Decimal(0)) @property def commission_rub(self) -> Decimal: # Prefer annotated field if hasattr(self, '_commission_rub'): return self._commission_rub return (self.price_rub * Decimal(settings.COMMISSION_OVER_150K) if self.price_rub > 150_000 else GlobalSettings.load().commission_rub) @property def preview_image(self): # Prefer annotated field if hasattr(self, '_images'): return next(filter(lambda x: x.is_preview, self._images), None) return self.images.filter(is_preview=True).first() @property def preview_image_url(self): return getattr(self.preview_image, 'image', None) @property def main_images(self): # Prefer prefetched field if hasattr(self, '_images'): return [img for img in self._images if not img.is_preview] return self.images.filter(is_preview=False) @property def title(self): return concat_not_null_values(self.brand, self.model) def __str__(self): return f'{self.id}' def save(self, *args, **kwargs): if self.id: old_obj = Checklist.objects.filter(id=self.id).first() # If status was updated, update status_updated_at field if old_obj and self.status != old_obj.status: self.status_updated_at = timezone.now() # Try to get CDEK barcode PDF if not self.cdek_barcode_pdf and self.cdek_tracking and self.status in Checklist.Status.PDF_AVAILABLE_STATUSES: from store.views import CDEKAPI pdf_file = CDEKAPI.client.get_barcode_file(self.cdek_tracking) if pdf_file: self.cdek_barcode_pdf.save(f'{self.id}_barcode.pdf', pdf_file) # Create preview image if self.images.exists() and not self.preview_image: # Render preview image original = self.images.first().image preview = create_preview(original.path, size=self.size, price_rub=self.price_rub, title=self.title) # Prepare bytes image_io = BytesIO() preview.save(image_io, format='JPEG') # Create Image model and save it image_obj = Image(is_preview=True, checklist_id=self.id) image_obj.image.save(name=f'{self.id}_preview.jpg', content=ContentFile(image_io.getvalue()), save=True) super().save(*args, **kwargs)