+ time_to_buy & buy_time_remaining fields
+ Category: hierarchy, removed slugs & added comission per category * Promocode value in rubles instead of percentage * Cleanup in GlobalSettings routes
This commit is contained in:
parent
e370a2097a
commit
3a6b06d223
|
|
@ -2,6 +2,7 @@
|
||||||
Django==4.2.2
|
Django==4.2.2
|
||||||
django-cleanup==8.0.0
|
django-cleanup==8.0.0
|
||||||
django-filter==23.2
|
django-filter==23.2
|
||||||
|
django-mptt==0.14.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
django-cors-headers==4.1.0
|
django-cors-headers==4.1.0
|
||||||
djoser==2.2.0
|
djoser==2.2.0
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import display
|
from django.contrib.admin import display
|
||||||
|
from mptt.admin import MPTTModelAdmin
|
||||||
|
|
||||||
|
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, User, Image, Client
|
||||||
|
|
||||||
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, User, Image
|
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, User, Image
|
||||||
|
|
||||||
|
|
@ -10,8 +13,8 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(MPTTModelAdmin):
|
||||||
list_display = ('slug', 'name')
|
list_display = ('name', 'delivery_price_CN_RU', 'commission')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,104 @@ from tqdm import tqdm
|
||||||
|
|
||||||
from store.models import Category, PaymentMethod
|
from store.models import Category, PaymentMethod
|
||||||
|
|
||||||
|
|
||||||
category_names = {
|
category_names = {
|
||||||
"shoes": "Обувь",
|
"Обувь": [
|
||||||
"outerwear": "Верхняя одежда",
|
"Кроссовки",
|
||||||
"underwear": "Нижнее белье",
|
"Кеды",
|
||||||
"bags": "Сумки",
|
"Пляжная обувь",
|
||||||
"cosmetics": "Косметика",
|
"Туфли",
|
||||||
"accessories": "Аксессуары",
|
"Босоножки",
|
||||||
"technics": "Техника",
|
"Сандвли",
|
||||||
"watches": "Часы",
|
"Лоаферы",
|
||||||
"toys": "Игрушки",
|
"Мокасины",
|
||||||
"home": "Товары для дома",
|
"Ботинки",
|
||||||
"foodndrinks": "Еда и напитки",
|
"Полуботинки",
|
||||||
"different": "Другое",
|
"Сапоги",
|
||||||
|
"Домашняя обувь",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Верхняя одежда": [
|
||||||
|
"Куртка летняя",
|
||||||
|
"Куртка зимняя",
|
||||||
|
"Худи",
|
||||||
|
"Толстовка",
|
||||||
|
"Футболка",
|
||||||
|
"Майка",
|
||||||
|
"Лонгслив",
|
||||||
|
"Рубашка",
|
||||||
|
"Платье",
|
||||||
|
"Жилетка",
|
||||||
|
"Свитер",
|
||||||
|
"Топ",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Нижнее белье": [
|
||||||
|
"Брюки",
|
||||||
|
"Штаны",
|
||||||
|
"Джинсы",
|
||||||
|
"Леггинсы",
|
||||||
|
"Шорты",
|
||||||
|
"Юбка",
|
||||||
|
"Колготки",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Сумки": [
|
||||||
|
"Женская сумка",
|
||||||
|
"Мужская сумка",
|
||||||
|
"Рюкзак",
|
||||||
|
"Кошелек",
|
||||||
|
"Визитница",
|
||||||
|
"Клатч",
|
||||||
|
"Шоппер",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Косметика": [
|
||||||
|
"Для лица",
|
||||||
|
"Для тела",
|
||||||
|
"Для губ",
|
||||||
|
"Для ресниц",
|
||||||
|
"Для волос",
|
||||||
|
"Для бровей",
|
||||||
|
"Для рук",
|
||||||
|
"Для ног",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Аксессуары": [
|
||||||
|
"Шарф",
|
||||||
|
"Шапка",
|
||||||
|
"Кепка",
|
||||||
|
"Очки",
|
||||||
|
"Украшения",
|
||||||
|
"Перчатки",
|
||||||
|
"Носки",
|
||||||
|
"Нижнее белье",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Техника": [
|
||||||
|
"Телефон",
|
||||||
|
"Планшет",
|
||||||
|
"Ноутбук",
|
||||||
|
"Приставка",
|
||||||
|
"Фен",
|
||||||
|
"Скайлер",
|
||||||
|
"Пылесос",
|
||||||
|
"Увлажнитель",
|
||||||
|
"Бытовая техника",
|
||||||
|
"Другое",
|
||||||
|
],
|
||||||
|
|
||||||
|
"Часы": ["Механические", "Электронные"],
|
||||||
|
"Игрушки": ["Lego", "Фигурка", "Мягкая", "Кукла", "Набор", "Другое"],
|
||||||
|
"Товары для дома": ["Полотенце", "Ковер", "Набор", "Косметичка", "Другое"],
|
||||||
|
"Еда и напитки": ["Еда", "Напиток"],
|
||||||
|
"Другое": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
payment_methods = {
|
payment_methods = {
|
||||||
|
|
@ -41,8 +126,10 @@ class Command(BaseCommand):
|
||||||
help = ''' Create root categories '''
|
help = ''' Create root categories '''
|
||||||
|
|
||||||
def create_categories(self):
|
def create_categories(self):
|
||||||
for slug, name in tqdm(category_names.items(), desc="Creating categories"):
|
for cat_name, subcat_names in tqdm(category_names.items(), desc="Creating categories"):
|
||||||
Category.objects.get_or_create(slug=slug, defaults={"name": name})
|
category, _ = Category.objects.get_or_create(name=cat_name, parent=None)
|
||||||
|
for subcat_name in subcat_names:
|
||||||
|
Category.objects.get_or_create(name=subcat_name, parent_id=category.id)
|
||||||
|
|
||||||
def create_payment_types(self):
|
def create_payment_types(self):
|
||||||
for slug, data in tqdm(payment_methods.items(), desc="Creating payment methods"):
|
for slug, data in tqdm(payment_methods.items(), desc="Creating payment methods"):
|
||||||
|
|
|
||||||
35
store/migrations/0041_remove_checklist_category_and_more.py
Normal file
35
store/migrations/0041_remove_checklist_category_and_more.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-08-17 23:19
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('store', '0040_alter_paymentmethod_cardnumber_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='checklist',
|
||||||
|
name='category',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='checklist',
|
||||||
|
name='subcategory',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='globalsettings',
|
||||||
|
name='time_to_buy',
|
||||||
|
field=models.DurationField(default=datetime.timedelta(seconds=10800), help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'", verbose_name='Время на покупку'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='promocode',
|
||||||
|
name='discount',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='Скидка в рублях'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Category',
|
||||||
|
),
|
||||||
|
]
|
||||||
39
store/migrations/0042_category_checklist_category.py
Normal file
39
store/migrations/0042_category_checklist_category.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-08-17 23:20
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('store', '0041_remove_checklist_category_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=20, verbose_name='Название')),
|
||||||
|
('delivery_price_CN_RU', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки Китай-РФ')),
|
||||||
|
('commission', models.DecimalField(decimal_places=2, default=0, max_digits=10, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Дополнительная комиссия, %')),
|
||||||
|
('lft', models.PositiveIntegerField(editable=False)),
|
||||||
|
('rght', models.PositiveIntegerField(editable=False)),
|
||||||
|
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||||
|
('level', models.PositiveIntegerField(editable=False)),
|
||||||
|
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='store.category', verbose_name='Родительская категория')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Категория',
|
||||||
|
'verbose_name_plural': 'Категории',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='checklist',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.category', verbose_name='Категория'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import math
|
import math
|
||||||
import posixpath
|
import posixpath
|
||||||
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.admin import display
|
from django.contrib.admin import display
|
||||||
|
|
@ -18,6 +19,8 @@ from django.db.models.lookups import GreaterThan
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_cleanup import cleanup
|
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 store.utils import create_preview, concat_not_null_values
|
||||||
from utils.cache import InMemoryCache
|
from utils.cache import InMemoryCache
|
||||||
|
|
@ -30,6 +33,9 @@ class GlobalSettings(models.Model):
|
||||||
delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
|
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)
|
commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
|
||||||
pickup_address = models.CharField('Адрес пункта самовывоза', max_length=200, blank=True, null=True)
|
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:
|
class Meta:
|
||||||
verbose_name = 'Глобальные настройки'
|
verbose_name = 'Глобальные настройки'
|
||||||
|
|
@ -57,14 +63,21 @@ class GlobalSettings(models.Model):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(MPTTModel):
|
||||||
name = models.CharField('Название', max_length=20)
|
name = models.CharField('Название', max_length=20)
|
||||||
slug = models.SlugField('Идентификатор', unique=True)
|
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
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class MPTTMeta:
|
||||||
|
order_insertion_by = ['id']
|
||||||
|
parent_attr = 'parent'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Категория'
|
verbose_name = 'Категория'
|
||||||
verbose_name_plural = 'Категории'
|
verbose_name_plural = 'Категории'
|
||||||
|
|
@ -138,8 +151,7 @@ class PromocodeQuerySet(models.QuerySet):
|
||||||
|
|
||||||
class Promocode(models.Model):
|
class Promocode(models.Model):
|
||||||
name = models.CharField('Название', max_length=100, unique=True)
|
name = models.CharField('Название', max_length=100, unique=True)
|
||||||
discount = models.DecimalField('Скидка', max_digits=10, decimal_places=2,
|
discount = models.PositiveIntegerField('Скидка в рублях')
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(100)])
|
|
||||||
free_delivery = models.BooleanField('Бесплатная доставка', default=False) # freedelivery
|
free_delivery = models.BooleanField('Бесплатная доставка', default=False) # freedelivery
|
||||||
no_comission = models.BooleanField('Без комиссии', default=False) # nocomission
|
no_comission = models.BooleanField('Без комиссии', default=False) # nocomission
|
||||||
is_active = models.BooleanField('Активен', default=True)
|
is_active = models.BooleanField('Активен', default=True)
|
||||||
|
|
@ -291,7 +303,6 @@ class Checklist(models.Model):
|
||||||
manager = models.ForeignKey('User', verbose_name='Менеджер', on_delete=models.SET_NULL, blank=True, null=True)
|
manager = models.ForeignKey('User', verbose_name='Менеджер', on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
product_link = models.URLField('Ссылка на товар', null=True, blank=True)
|
product_link = models.URLField('Ссылка на товар', null=True, blank=True)
|
||||||
category = models.ForeignKey('Category', verbose_name="Категория", blank=True, null=True, on_delete=models.SET_NULL)
|
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)
|
brand = models.CharField('Бренд', max_length=100, null=True, blank=True)
|
||||||
model = models.CharField('Модель', max_length=100, null=True, blank=True)
|
model = models.CharField('Модель', max_length=100, null=True, blank=True)
|
||||||
|
|
@ -341,6 +352,16 @@ class Checklist(models.Model):
|
||||||
verbose_name = 'Заказ'
|
verbose_name = 'Заказ'
|
||||||
verbose_name_plural = 'Заказы'
|
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
|
@property
|
||||||
def price_rub(self) -> int:
|
def price_rub(self) -> int:
|
||||||
# Prefer annotated field
|
# Prefer annotated field
|
||||||
|
|
@ -361,7 +382,7 @@ class Checklist(models.Model):
|
||||||
# and intentionally don't check if promocode is active here.
|
# and intentionally don't check if promocode is active here.
|
||||||
# It's also good for archive orders.
|
# It's also good for archive orders.
|
||||||
promocode = self.promocode
|
promocode = self.promocode
|
||||||
price -= promocode.discount * self.price_rub / 100
|
price -= min(self.price_rub, promocode.discount)
|
||||||
|
|
||||||
free_delivery = promocode.free_delivery
|
free_delivery = promocode.free_delivery
|
||||||
no_comission = promocode.no_comission
|
no_comission = promocode.no_comission
|
||||||
|
|
@ -372,6 +393,11 @@ class Checklist(models.Model):
|
||||||
if not no_comission:
|
if not no_comission:
|
||||||
price += self.commission_rub
|
price += self.commission_rub
|
||||||
|
|
||||||
|
# Add commission of bottom-most 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))
|
return max(0, math.ceil(price))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
from drf_extra_fields.fields import Base64ImageField
|
from drf_extra_fields.fields import Base64ImageField
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from store.exceptions import CRMException, InvalidCredentialsException
|
|
||||||
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image
|
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image
|
||||||
|
from store.utils import get_primary_key_related_model
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -39,12 +38,36 @@ class ImageListSerializer(serializers.ListSerializer):
|
||||||
return [image['image'] for image in images]
|
return [image['image'] for image in images]
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryChecklistSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
fields = ('id', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
class CategorySerializer(serializers.ModelSerializer):
|
||||||
|
chinarush = serializers.DecimalField(source='delivery_price_CN_RU', max_digits=10, decimal_places=2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
fields = ('id', 'name', 'chinarush', 'commission')
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryFullSerializer(CategorySerializer):
|
||||||
|
children = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_children(self, obj):
|
||||||
|
return CategoryFullSerializer(obj.get_children(), many=True).data
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CategorySerializer.Meta.model
|
||||||
|
fields = CategorySerializer.Meta.fields + ('children',)
|
||||||
|
|
||||||
|
|
||||||
class ChecklistSerializer(serializers.ModelSerializer):
|
class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.CharField(read_only=True)
|
id = serializers.CharField(read_only=True)
|
||||||
managerid = serializers.PrimaryKeyRelatedField(source='manager_id', read_only=True, allow_null=True)
|
managerid = serializers.PrimaryKeyRelatedField(source='manager_id', read_only=True, allow_null=True)
|
||||||
link = serializers.URLField(source='product_link', required=False)
|
link = serializers.URLField(source='product_link', required=False)
|
||||||
category = serializers.SlugRelatedField(slug_field='slug', queryset=Category.objects.all(),
|
category = get_primary_key_related_model(CategoryChecklistSerializer, required=False, allow_null=True)
|
||||||
required=False, allow_null=True)
|
|
||||||
|
|
||||||
image = ImageListSerializer(source='main_images', required=False)
|
image = ImageListSerializer(source='main_images', required=False)
|
||||||
previewimage = serializers.ImageField(source='preview_image_url', read_only=True)
|
previewimage = serializers.ImageField(source='preview_image_url', read_only=True)
|
||||||
|
|
@ -156,7 +179,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checklist
|
model = Checklist
|
||||||
fields = ('id', 'status', 'managerid', 'link',
|
fields = ('id', 'status', 'managerid', 'link',
|
||||||
'category', 'subcategory',
|
'category',
|
||||||
'brand', 'model', 'size',
|
'brand', 'model', 'size',
|
||||||
'image',
|
'image',
|
||||||
'previewimage',
|
'previewimage',
|
||||||
|
|
@ -168,7 +191,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
'receivername', 'reveiverphone',
|
'receivername', 'reveiverphone',
|
||||||
'split', 'paymenttype', 'paymentprovement', 'checkphoto',
|
'split', 'paymenttype', 'paymentprovement', 'checkphoto',
|
||||||
'trackid', 'cdek_tracking', 'cdek_barcode_pdf', 'delivery', 'delivery_display',
|
'trackid', 'cdek_tracking', 'cdek_barcode_pdf', 'delivery', 'delivery_display',
|
||||||
'startDate', 'currentDate',
|
'startDate', 'currentDate', 'buy_time_remaining'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -183,38 +206,15 @@ class AnonymousUserChecklistSerializer(ChecklistSerializer):
|
||||||
'recievername', 'recieverphone', 'tg'})
|
'recievername', 'recieverphone', 'tg'})
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsYuanRateSerializer(serializers.ModelSerializer):
|
class GlobalSettingsSerializer(serializers.ModelSerializer):
|
||||||
currency = serializers.DecimalField(source='yuan_rate', max_digits=10, decimal_places=2)
|
currency = serializers.DecimalField(source='yuan_rate', max_digits=10, decimal_places=2)
|
||||||
|
chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2)
|
||||||
class Meta:
|
commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2)
|
||||||
model = GlobalSettings
|
|
||||||
fields = ('currency',)
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsPickupSerializer(serializers.ModelSerializer):
|
|
||||||
pickup = serializers.CharField(source='pickup_address')
|
pickup = serializers.CharField(source='pickup_address')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GlobalSettings
|
model = GlobalSettings
|
||||||
fields = ('pickup',)
|
fields = ('currency', 'commission', 'chinadelivery', 'pickup', 'time_to_buy')
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsPriceSerializer(serializers.ModelSerializer):
|
|
||||||
commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2)
|
|
||||||
chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = GlobalSettings
|
|
||||||
fields = ('commission', 'chinadelivery')
|
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(serializers.ModelSerializer):
|
|
||||||
category = serializers.CharField(source='slug')
|
|
||||||
chinarush = serializers.DecimalField(source='delivery_price_CN_RU', max_digits=10, decimal_places=2)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Category
|
|
||||||
fields = ('category', 'chinarush')
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodSerializer(serializers.ModelSerializer):
|
class PaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ urlpatterns = [
|
||||||
path("checklist/", views.ChecklistAPI.as_view()),
|
path("checklist/", views.ChecklistAPI.as_view()),
|
||||||
path("checklist/<str:id>", views.ChecklistAPI.as_view()),
|
path("checklist/<str:id>", views.ChecklistAPI.as_view()),
|
||||||
|
|
||||||
path("currency/", views.YuanRateAPI.as_view()),
|
|
||||||
path("category/", views.CategoryAPI.as_view()),
|
path("category/", views.CategoryAPI.as_view()),
|
||||||
path("pickup/", views.PickupAPI.as_view()),
|
path("category/<int:id>", views.CategoryAPI.as_view()),
|
||||||
path("category/price/", views.PricesAPI.as_view()),
|
|
||||||
path("payment/", views.PaymentMethodsAPI.as_view()),
|
path("payment/", views.PaymentMethodsAPI.as_view()),
|
||||||
|
path("settings/", views.GlobalSettingsAPI.as_view()),
|
||||||
|
|
||||||
path("promo/", views.PromoCodeAPI.as_view()),
|
path("promo/", views.PromoCodeAPI.as_view()),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
|
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
|
||||||
|
|
||||||
from poizonstore.settings import BASE_DIR
|
from poizonstore.settings import BASE_DIR
|
||||||
|
|
@ -125,3 +126,30 @@ def create_preview(source_image: str, size=None, price_rub=None, title_lines=Non
|
||||||
|
|
||||||
def concat_not_null_values(*values, separator=' '):
|
def concat_not_null_values(*values, separator=' '):
|
||||||
return separator.join([v for v in values if v is not None])
|
return separator.join([v for v in values if v is not None])
|
||||||
|
|
||||||
|
|
||||||
|
def get_primary_key_related_model(model_class, **kwargs):
|
||||||
|
"""
|
||||||
|
Info: https://stackoverflow.com/a/43742949
|
||||||
|
Nested serializers are a mess.
|
||||||
|
This lets us accept ids when saving / updating instead of nested objects.
|
||||||
|
Representation would be into an object (depending on model_class).
|
||||||
|
"""
|
||||||
|
class PrimaryKeyNestedMixin(model_class):
|
||||||
|
default_error_messages = {
|
||||||
|
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
|
||||||
|
'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
|
return model_class.Meta.model.objects.get(pk=data)
|
||||||
|
except model_class.Meta.model.DoesNotExist:
|
||||||
|
self.fail('does_not_exist', pk_value=data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||||
|
|
||||||
|
def to_representation(self, data):
|
||||||
|
return model_class.to_representation(self, data)
|
||||||
|
|
||||||
|
return PrimaryKeyNestedMixin(**kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import calendar
|
import calendar
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.db.models import F, Count, Sum
|
from django.db.models import F, Count, Sum
|
||||||
from django.utils import timezone
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import generics, permissions, mixins, status, viewsets
|
from rest_framework import generics, permissions, mixins, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
@ -15,9 +15,9 @@ from rest_framework.response import Response
|
||||||
from cdek.api import CDEKClient
|
from cdek.api import CDEKClient
|
||||||
from store.exceptions import CRMException
|
from store.exceptions import CRMException
|
||||||
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode
|
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode
|
||||||
from store.serializers import (ChecklistSerializer, GlobalSettingsYuanRateSerializer,
|
from store.serializers import (ChecklistSerializer, CategorySerializer, CategoryFullSerializer,
|
||||||
CategorySerializer, GlobalSettingsPriceSerializer, PaymentMethodSerializer,
|
PaymentMethodSerializer, GlobalSettingsSerializer,
|
||||||
PromocodeSerializer, GlobalSettingsPickupSerializer, AnonymousUserChecklistSerializer)
|
PromocodeSerializer, AnonymousUserChecklistSerializer)
|
||||||
from utils.permissions import ReadOnly
|
from utils.permissions import ReadOnly
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,10 +63,9 @@ class ChecklistAPI(mixins.ListModelMixin,
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
obj: Checklist = super().get_object()
|
obj: Checklist = super().get_object()
|
||||||
|
|
||||||
# 3 hours maximum in 'neworder' status -> move to drafts
|
# N time maximum in 'neworder' status -> move to drafts
|
||||||
if obj.status == Checklist.Status.NEW:
|
if obj.status == Checklist.Status.NEW and obj.buy_time_remaining is not None:
|
||||||
diff_hours = (timezone.now() - obj.status_updated_at).seconds / 3600
|
if obj.buy_time_remaining <= timedelta():
|
||||||
if diff_hours > 3:
|
|
||||||
obj.status = Checklist.Status.DRAFT
|
obj.status = Checklist.Status.DRAFT
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
@ -95,67 +94,23 @@ class ChecklistAPI(mixins.ListModelMixin,
|
||||||
return self.destroy(request, *args, **kwargs)
|
return self.destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class YuanRateAPI(generics.GenericAPIView):
|
class CategoryAPI(mixins.ListModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView):
|
||||||
serializer_class = GlobalSettingsYuanRateSerializer
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
return GlobalSettings.load()
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
yuan_rate = GlobalSettings.load().yuan_rate
|
|
||||||
return Response(data={'currency': yuan_rate})
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryAPI(generics.GenericAPIView):
|
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
|
lookup_field = 'id'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Category.objects.all()
|
return Category.objects.root_nodes()
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
categories_qs = self.get_queryset()
|
categories_qs = self.get_queryset()
|
||||||
global_settings = GlobalSettings.load()
|
return Response(CategoryFullSerializer(categories_qs, many=True).data)
|
||||||
|
|
||||||
return Response({
|
|
||||||
'categories': CategorySerializer(categories_qs, many=True).data,
|
|
||||||
'prices': GlobalSettingsPriceSerializer(global_settings).data,
|
|
||||||
})
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
data = request.data
|
return self.partial_update(request, *args, **kwargs)
|
||||||
if not all(k in data for k in ("category", "chinarush")):
|
|
||||||
raise CRMException('category and chinarush is required')
|
|
||||||
|
|
||||||
instance = get_object_or_404(self.get_queryset(), slug=data['category'])
|
|
||||||
serializer = self.get_serializer(instance, data=data, partial=True)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class PricesAPI(generics.GenericAPIView):
|
class GlobalSettingsAPI(generics.GenericAPIView):
|
||||||
serializer_class = GlobalSettingsPriceSerializer
|
serializer_class = GlobalSettingsSerializer
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
instance = GlobalSettings.load()
|
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class PickupAPI(DisablePermissionsMixin):
|
|
||||||
serializer_class = GlobalSettingsPickupSerializer
|
|
||||||
permission_classes = [IsAuthenticated | ReadOnly]
|
permission_classes = [IsAuthenticated | ReadOnly]
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user