628 lines
24 KiB
Python
628 lines
24 KiB
Python
import math
|
||
import posixpath
|
||
from datetime import timedelta
|
||
from decimal import Decimal
|
||
import random
|
||
import string
|
||
from io import BytesIO
|
||
from typing import Optional
|
||
|
||
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, Max, Q, ExpressionWrapper
|
||
from django.db.models.functions import Ceil
|
||
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 mptt.fields import TreeForeignKey
|
||
from mptt.models import MPTTModel
|
||
|
||
from store.utils import create_preview, concat_not_null_values
|
||
from utils.cache import InMemoryCache
|
||
|
||
|
||
class GlobalSettings(models.Model):
|
||
# 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)
|
||
time_to_buy = models.DurationField('Время на покупку',
|
||
help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'",
|
||
default=timedelta(hours=3))
|
||
|
||
class Meta:
|
||
verbose_name = 'Глобальные настройки'
|
||
verbose_name_plural = 'Глобальные настройки'
|
||
|
||
def save(self, *args, **kwargs):
|
||
# Store only one instance of GlobalSettings
|
||
self.id = 1
|
||
self.__class__.objects.exclude(id=self.id).delete()
|
||
super().save(*args, **kwargs)
|
||
|
||
InMemoryCache.set('GlobalSettings', self)
|
||
|
||
def __str__(self) -> str:
|
||
return f'GlobalSettings <{self.id}>'
|
||
|
||
@classmethod
|
||
def load(cls) -> 'GlobalSettings':
|
||
cached = InMemoryCache.get('GlobalSettings')
|
||
if cached:
|
||
return cached
|
||
|
||
obj, _ = cls.objects.get_or_create(id=1)
|
||
InMemoryCache.set('GlobalSettings', obj)
|
||
return obj
|
||
|
||
|
||
class Category(MPTTModel):
|
||
name = models.CharField('Название', max_length=20)
|
||
parent = TreeForeignKey('self', verbose_name='Родительская категория', on_delete=models.SET_NULL, blank=True, null=True, related_name='children', db_index=True)
|
||
delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0) # Chinadelivery2
|
||
commission = models.DecimalField('Дополнительная комиссия, %',
|
||
max_digits=10, decimal_places=2, default=0,
|
||
validators=[MinValueValidator(0), MaxValueValidator(100)])
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class MPTTMeta:
|
||
order_insertion_by = ['id']
|
||
parent_attr = 'parent'
|
||
|
||
class Meta:
|
||
verbose_name = 'Категория'
|
||
verbose_name_plural = 'Категории'
|
||
|
||
@property
|
||
def delivery_price(self):
|
||
if not self.delivery_price_CN_RU and self.parent_id:
|
||
return self.parent.delivery_price
|
||
else:
|
||
return self.delivery_price_CN_RU
|
||
|
||
@property
|
||
def commission_price(self):
|
||
""" Get commission from object or from its parent """
|
||
if not self.commission and self.parent_id:
|
||
return self.parent.commission_price
|
||
else:
|
||
return self.commission
|
||
|
||
|
||
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.PositiveIntegerField('Скидка в рублях')
|
||
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, blank=True, null=True)
|
||
requisites = models.CharField('Реквизиты', max_length=200, blank=True, null=True)
|
||
|
||
class Meta:
|
||
verbose_name = 'Метод оплаты'
|
||
verbose_name_plural = 'Методы оплаты'
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
def image_upload_path(instance, filename):
|
||
dirname = Image.TYPE_TO_UPLOAD_PATH[instance.type]
|
||
return posixpath.join(dirname, filename)
|
||
|
||
|
||
@cleanup.select
|
||
class Image(models.Model):
|
||
DEFAULT = 0
|
||
PREVIEW = 1
|
||
DOC = 2
|
||
GIFT = 3
|
||
|
||
TYPE_CHOICES = (
|
||
(DEFAULT, 'Изображение'),
|
||
(PREVIEW, 'Превью'),
|
||
(DOC, 'Документ'),
|
||
(GIFT, 'Подарок'),
|
||
)
|
||
|
||
TYPE_TO_UPLOAD_PATH = {
|
||
DEFAULT: 'checklist_images/',
|
||
PREVIEW: 'checklist_images/',
|
||
DOC: 'docs/',
|
||
GIFT: 'gifts/',
|
||
}
|
||
|
||
image = models.ImageField('Файл изображения', upload_to=image_upload_path)
|
||
type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=DEFAULT)
|
||
|
||
class Meta:
|
||
verbose_name = 'Изображение'
|
||
verbose_name_plural = 'Изображения'
|
||
|
||
def __str__(self):
|
||
return f"{self.get_type_display()}: {getattr(self.image, 'name', '')}"
|
||
|
||
|
||
class Gift(models.Model):
|
||
name = models.CharField('Название', max_length=100)
|
||
image = models.ImageField('Фото', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.GIFT], null=True, blank=True)
|
||
min_price = models.DecimalField('Минимальная цена в юанях', help_text='от какой суммы доступен подарок', max_digits=10, decimal_places=2, default=0)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = 'Подарок'
|
||
verbose_name_plural = 'Подарки'
|
||
|
||
|
||
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', 'price_snapshot', 'gift') \
|
||
.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):
|
||
return self.annotate(
|
||
_yuan_rate=Case(
|
||
When(price_snapshot_id__isnull=False, then=F('price_snapshot__yuan_rate')),
|
||
default=GlobalSettings.load().yuan_rate
|
||
),
|
||
_price_rub=Ceil(F('_yuan_rate') * F('price_yuan'))
|
||
)
|
||
|
||
def annotate_commission_rub(self):
|
||
default_commission = GlobalSettings.load().commission_rub
|
||
over_150k_commission = F('_price_rub') * settings.COMMISSION_OVER_150K
|
||
|
||
category_commission_is_zero_and_parent_present = (
|
||
(Q(category__commission__isnull=True) | Q(category__commission=0)) & Q(category__parent__isnull=False)
|
||
)
|
||
|
||
return self.annotate(
|
||
_category_commission_percent=Case(
|
||
When(category_commission_is_zero_and_parent_present, then=F('category__parent__commission')),
|
||
default=F('category__commission')
|
||
),
|
||
|
||
_category_commission=F('_category_commission_percent') * F('_price_rub') / 100,
|
||
|
||
_over_150k_commission=Case(
|
||
When(GreaterThan(F("_price_rub"), 150_000), then=over_150k_commission),
|
||
default=0,
|
||
output_field=DecimalField()
|
||
),
|
||
|
||
_commission_rub=Case(
|
||
When(price_snapshot_id__isnull=False, then=F('price_snapshot__commission_rub')),
|
||
default=Max(default_commission, F('_over_150k_commission'), F('_category_commission')),
|
||
output_field=DecimalField()
|
||
),
|
||
)
|
||
|
||
|
||
class PriceSnapshot(models.Model):
|
||
yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
|
||
delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
|
||
delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0)
|
||
commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
|
||
|
||
|
||
@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"
|
||
CDEK_COURIER = "cdek_courier"
|
||
|
||
CHOICES = (
|
||
(PICKUP, 'Самовывоз из шоурума'),
|
||
(CDEK, 'Пункт выдачи заказов CDEK'),
|
||
(CDEK_COURIER, 'Курьерская доставка 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)
|
||
|
||
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)
|
||
gift = models.ForeignKey('Gift', verbose_name='Подарок', on_delete=models.SET_NULL, 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)
|
||
|
||
images = models.ManyToManyField('Image', verbose_name='Изображения', related_name='+', blank=True)
|
||
payment_proof = models.ImageField('Подтверждение оплаты', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC],
|
||
null=True, blank=True) # paymentprovement
|
||
split_payment_proof = models.ImageField('Подтверждение оплаты сплита',
|
||
upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC],
|
||
null=True, blank=True)
|
||
split_accepted = models.BooleanField('Сплит принят', default=False)
|
||
|
||
receipt = models.ImageField('Фото чека', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC], null=True, blank=True) # checkphoto
|
||
|
||
delivery = models.CharField('Тип доставки', max_length=15, 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)
|
||
|
||
price_snapshot = models.ForeignKey('PriceSnapshot', verbose_name='Сохраненные цены',
|
||
related_name='checklist',
|
||
on_delete=models.SET_NULL, null=True, blank=True)
|
||
|
||
objects = ChecklistQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
verbose_name = 'Заказ'
|
||
verbose_name_plural = 'Заказы'
|
||
|
||
@property
|
||
def buy_time_remaining(self) -> Optional[timedelta]:
|
||
if self.status != Checklist.Status.NEW:
|
||
return None
|
||
|
||
time_to_buy = GlobalSettings.load().time_to_buy
|
||
diff = max(timedelta(), timezone.now() - self.status_updated_at)
|
||
result = max(timedelta(), time_to_buy - diff)
|
||
return result
|
||
|
||
@property
|
||
def price_rub(self) -> int:
|
||
# Prefer annotated field for calculation
|
||
if hasattr(self, '_price_rub'):
|
||
return self._price_rub
|
||
|
||
# Get saved prices
|
||
if self.price_snapshot_id:
|
||
yuan_rate = self.price_snapshot.yuan_rate
|
||
else:
|
||
yuan_rate = GlobalSettings.load().yuan_rate
|
||
|
||
return math.ceil(yuan_rate * self.price_yuan)
|
||
|
||
@property
|
||
def full_price(self) -> int:
|
||
price = self.price_rub
|
||
|
||
free_delivery = False
|
||
no_comission = False
|
||
|
||
if self.promocode_id:
|
||
# We assume that working promocode was bound correctly through serializer
|
||
# and intentionally don't check if promocode is active here.
|
||
# It's also good for archive orders.
|
||
promocode = self.promocode
|
||
price -= min(self.price_rub, promocode.discount)
|
||
|
||
free_delivery = promocode.free_delivery
|
||
no_comission = promocode.no_comission
|
||
|
||
if not free_delivery:
|
||
price += self.delivery_price_CN + self.delivery_price_CN_RU
|
||
|
||
if not no_comission:
|
||
price += self.commission_rub
|
||
|
||
return max(0, math.ceil(price))
|
||
|
||
@property
|
||
def yuan_rate(self) -> Decimal:
|
||
# Get saved value if exists
|
||
if self.price_snapshot_id:
|
||
return self.price_snapshot.yuan_rate
|
||
else:
|
||
return GlobalSettings.load().yuan_rate
|
||
|
||
@property
|
||
def delivery_price_CN(self) -> Decimal:
|
||
# Get saved value if exists
|
||
if self.price_snapshot_id:
|
||
return self.price_snapshot.delivery_price_CN
|
||
else:
|
||
return GlobalSettings.load().delivery_price_CN
|
||
|
||
@property
|
||
def delivery_price_CN_RU(self) -> Decimal:
|
||
# Get saved value if exists
|
||
if self.price_snapshot_id:
|
||
return self.price_snapshot.delivery_price_CN_RU
|
||
else:
|
||
return getattr(self.category, 'delivery_price', Decimal(0))
|
||
|
||
@property
|
||
def commission_rub(self) -> Decimal:
|
||
# Prefer annotated field
|
||
if hasattr(self, '_commission_rub'):
|
||
return self._commission_rub
|
||
|
||
# Prefer saved value
|
||
if self.price_snapshot_id:
|
||
return self.price_snapshot.commission_rub
|
||
|
||
# Default commission
|
||
commission = GlobalSettings.load().commission_rub
|
||
|
||
if self.price_rub > 150_000:
|
||
commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))
|
||
|
||
if self.category_id:
|
||
# Add commission of bottom-most category
|
||
category_commission = getattr(self.category, 'commission_price', 0)
|
||
commission = max(commission, category_commission * self.price_rub / 100)
|
||
|
||
return commission
|
||
|
||
@property
|
||
def preview_image(self):
|
||
# Prefer annotated field
|
||
if hasattr(self, '_images'):
|
||
# Get first preview image from all images
|
||
return next(filter(lambda x: x.type == Image.PREVIEW, self._images), None)
|
||
|
||
return self.images.filter(type=Image.PREVIEW).first()
|
||
|
||
@property
|
||
def preview_image_url(self):
|
||
return getattr(self.preview_image, 'image', None)
|
||
|
||
@property
|
||
def main_images(self) -> list:
|
||
# Prefer prefetched field
|
||
if hasattr(self, '_images'):
|
||
return [img for img in self._images if img.type == Image.DEFAULT]
|
||
|
||
return self.images.filter(type=Image.DEFAULT)
|
||
|
||
def __str__(self):
|
||
return f'{self.id}'
|
||
|
||
def generate_preview(self, source_img: Image = None):
|
||
source_img = source_img or next(iter(self.main_images), None)
|
||
if not source_img:
|
||
return
|
||
|
||
title_lines = [v for v in (self.brand, self.model) if v is not None]
|
||
# Render preview image
|
||
preview = create_preview(source_img.image.path,
|
||
size=self.size,
|
||
price_rub=self.full_price,
|
||
title_lines=title_lines)
|
||
|
||
# Prepare bytes
|
||
image_io = BytesIO()
|
||
preview.save(image_io, format='JPEG')
|
||
|
||
# Create Image model and save it
|
||
image_obj = Image(type=Image.PREVIEW)
|
||
image_obj.image.save(name=f'{self.id}_preview.jpg',
|
||
content=ContentFile(image_io.getvalue()),
|
||
save=True)
|
||
|
||
self.images.add(image_obj)
|
||
|
||
def save_prices(self):
|
||
# Temporarily remove snapshot from object
|
||
self.price_snapshot = None
|
||
|
||
snapshot, _ = PriceSnapshot.objects.get_or_create(
|
||
checklist__id=self.id,
|
||
defaults={
|
||
'yuan_rate': self.yuan_rate,
|
||
'delivery_price_CN': self.delivery_price_CN,
|
||
'delivery_price_CN_RU': self.delivery_price_CN_RU,
|
||
'commission_rub': self.commission_rub,
|
||
}
|
||
)
|
||
|
||
# Restore snapshot
|
||
self.price_snapshot = snapshot
|
||
|
||
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()
|
||
|
||
# Invalidate old CDEK barcode PDF
|
||
if not self.cdek_barcode_pdf or self.cdek_tracking != old_obj.cdek_tracking:
|
||
self.cdek_barcode_pdf.delete(save=False)
|
||
self.cdek_barcode_pdf = None
|
||
|
||
# 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, save=False)
|
||
|
||
# Create preview image
|
||
if not self.preview_image:
|
||
self.generate_preview()
|
||
|
||
# Save price details to snapshot
|
||
if self.price_snapshot_id:
|
||
# Status updated from other statuses back to DRAFT
|
||
if self.status == Checklist.Status.DRAFT:
|
||
self.price_snapshot.delete()
|
||
self.price_snapshot = None
|
||
|
||
elif self.status != Checklist.Status.DRAFT:
|
||
self.save_prices()
|
||
|
||
super().save(*args, **kwargs)
|
||
|