diff --git a/store/migrations/0043_pricesnapshot_checklist_price_snapshot.py b/store/migrations/0043_pricesnapshot_checklist_price_snapshot.py new file mode 100644 index 0000000..3a335d7 --- /dev/null +++ b/store/migrations/0043_pricesnapshot_checklist_price_snapshot.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.2 on 2023-10-04 02:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('store', '0042_oldchecklist_checklist_split_payment_proof_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PriceSnapshot', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yuan_rate', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Курс CNY/RUB')), + ('delivery_price_CN', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки по Китаю')), + ('delivery_price_CN_RU', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки Китай-РФ')), + ('commission_rub', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Комиссия, руб')), + ], + ), + migrations.AddField( + model_name='checklist', + name='price_snapshot', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checklist', to='store.pricesnapshot', verbose_name='Сохраненные цены'), + ), + ] diff --git a/store/models.py b/store/models.py index cdab3fe..f964334 100644 --- a/store/models.py +++ b/store/models.py @@ -228,17 +228,23 @@ def generate_checklist_id(): class ChecklistQuerySet(models.QuerySet): def with_base_related(self): - return self.select_related('manager', 'category', 'payment_method', 'promocode')\ + return self.select_related('manager', 'category', 'payment_method', 'promocode', 'price_snapshot')\ .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): + # FIXME: implement price_rub in DB query + return self + yuan_rate = GlobalSettings.load().yuan_rate return self.annotate(_price_rub=F('price_yuan') * yuan_rate) def annotate_commission_rub(self): + # FIXME: implement category commission in DB query + return 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), @@ -247,6 +253,13 @@ class ChecklistQuerySet(models.QuerySet): )) +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 @@ -350,6 +363,10 @@ class Checklist(models.Model): 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: @@ -368,11 +385,18 @@ class Checklist(models.Model): @property def price_rub(self) -> int: - # Prefer annotated field - if hasattr(self, '_price_rub'): - return self._price_rub + # FIXME: implement price_rub in DB query + # # Prefer annotated field for calculation + # if hasattr(self, '_price_rub'): + # return self._price_rub - return math.ceil(GlobalSettings.load().yuan_rate * self.price_yuan) + # 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: @@ -392,32 +416,58 @@ class Checklist(models.Model): no_comission = promocode.no_comission if not free_delivery: - price += GlobalSettings.load().delivery_price_CN + self.delivery_price_CN_RU + price += self.delivery_price_CN + self.delivery_price_CN_RU if not no_comission: price += self.commission_rub - # Add commission of bottom-most category - if self.category: - category = self.category.get_ancestors(ascending=True, include_self=True).first() - category_commission = getattr(category, 'commission', 0) - price += category_commission * self.price_rub / 100 - 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: - return getattr(self.category, 'delivery_price_CN_RU', Decimal(0)) + # 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_CN_RU', Decimal(0)) @property def commission_rub(self) -> Decimal: - # Prefer annotated field - if hasattr(self, '_commission_rub'): - return self._commission_rub + # FIXME: implement category commission in DB query + # # 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) + # Prefer saved value + if self.price_snapshot_id: + return self.price_snapshot.commission_rub + + commission = (self.price_rub * Decimal(settings.COMMISSION_OVER_150K) + if self.price_rub > 150_000 + else GlobalSettings.load().commission_rub) + + # Add commission of bottom-most category + if self.category_id: + category_commission = getattr(self.category, 'commission', 0) + commission += category_commission * self.price_rub / 100 + + return commission @property def preview_image(self): @@ -467,9 +517,27 @@ class Checklist(models.Model): 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.price_snapshot_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() @@ -485,5 +553,15 @@ class Checklist(models.Model): 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) diff --git a/store/serializers.py b/store/serializers.py index 6f53bfb..1628121 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -75,16 +75,16 @@ class ChecklistSerializer(serializers.ModelSerializer): promo = serializers.SlugRelatedField(source='promocode', slug_field='name', queryset=Promocode.objects.active(), required=False, allow_null=True) - currency = serializers.SerializerMethodField('get_yuan_rate') + currency = serializers.DecimalField(source='yuan_rate', read_only=True, max_digits=10, decimal_places=2) curencycurency2 = serializers.DecimalField(source='price_yuan', required=False, max_digits=10, decimal_places=2) currency3 = serializers.IntegerField(source='price_rub', read_only=True) - chinadelivery = serializers.SerializerMethodField('get_delivery_price_CN', read_only=True) - chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', read_only=True, max_digits=10, - decimal_places=2) + chinadelivery = serializers.DecimalField(source='delivery_price_CN', read_only=True, max_digits=10, decimal_places=2) + chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', read_only=True, + max_digits=10, decimal_places=2) fullprice = serializers.IntegerField(source='full_price', read_only=True) realprice = serializers.DecimalField(source='real_price', required=False, allow_null=True, max_digits=10, decimal_places=2) - commission = serializers.SerializerMethodField('get_commission', read_only=True) + commission = serializers.DecimalField(source='commission_rub', read_only=True, max_digits=10, decimal_places=2) buyername = serializers.CharField(source='buyer_name', required=False, allow_null=True) buyerphone = serializers.CharField(source='buyer_phone', required=False, allow_null=True) @@ -151,22 +151,10 @@ class ChecklistSerializer(serializers.ModelSerializer): return instance - @staticmethod - def get_yuan_rate(obj: Checklist): - return GlobalSettings.load().yuan_rate - @staticmethod def get_image(obj: Checklist): return obj.images.all() - @staticmethod - def get_delivery_price_CN(obj: Checklist): - return GlobalSettings.load().delivery_price_CN - - @staticmethod - def get_commission(obj: Checklist): - return GlobalSettings.load().commission_rub - class Meta: model = Checklist fields = ('id', 'status', 'managerid', 'link',