Initial commit

This commit is contained in:
Phil Zhitnikov 2023-07-03 06:38:55 +04:00
commit 1d615b2735
41 changed files with 1819 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
# Static stuff
!assets
!media
media/**/*
assets/**/*
env
.idea
.DS_Store
db.sqlite3

BIN
_docs/cdekapi.pdf Normal file

Binary file not shown.

Binary file not shown.

BIN
_docs/ТЗ Rest Api.pdf Normal file

Binary file not shown.

Binary file not shown.

0
cdek/__init__.py Normal file
View File

89
cdek/api.py Normal file
View File

@ -0,0 +1,89 @@
import http
import os
from urllib.parse import urljoin
import requests
from django.conf import settings
from requests import Request
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
class CDEKClient:
AUTH_ENDPOINT = 'oauth/token'
ORDER_INFO_ENDPOINT = 'orders'
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
MAX_RETRIES = 2
def __init__(self, client_id, client_secret, grant_type='client_credentials'):
self.api_url = 'https://api.cdek.ru/v2/'
self.client_id = client_id
self.client_secret = client_secret
self.grant_type = grant_type
self.session = requests.Session()
def request(self, method, url, *args, **kwargs):
joined_url = urljoin(self.api_url, url)
request = Request(method, joined_url, *args, **kwargs)
prepared = self.session.prepare_request(request)
retries = 0
while retries < self.MAX_RETRIES:
r = self.session.send(prepared)
# TODO: handle/log errors
if r.status_code == http.HTTPStatus.UNAUTHORIZED:
self.authorize()
continue
return r
def authorize(self):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': self.grant_type
}
r = self.request('POST', self.AUTH_ENDPOINT, params=params)
if r:
data = r.json()
token = data['access_token']
self.session.headers.update({'Authorization': f'Bearer {token}'})
# FIXME: not working?
def get_order_info(self, cdek_number):
params = {
'cdek_number': str(cdek_number)
}
return self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
def calculate_tariff(self, order_data):
return self.request('POST', self.CALCULATOR_TARIFF_ENDPOINT, json=order_data)
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
client.authorize()
order_data = {
"type": "1",
"currency": "1",
"lang": "rus",
"tariff_code": "137",
"from_location": {
"address": "Санкт-Петербург, Невский пр. 30"
},
"to_location": {
"address": "Тверь, ул 15 лет октября, дом 50, кВ 2"
},
"packages": [
{
"weight": 1200,
"length": 35,
"width": 26,
"height": 14
}
]
}
r = client.calculate_tariff(order_data)
print(r)

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
poizonstore/__init__.py Normal file
View File

16
poizonstore/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for poizonstore project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
application = get_asgi_application()

163
poizonstore/settings.py Normal file
View File

@ -0,0 +1,163 @@
"""
Django settings for poizonstore project.
Generated by 'django-admin startproject' using Django 4.2.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '***REMOVED***'
CDEK_CLIENT_ID = '***REMOVED***'
CDEK_CLIENT_SECRET = '***REMOVED***'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
INTERNAL_IPS = ["127.0.0.1"]
AUTH_USER_MODEL = 'store.User'
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_cleanup.apps.CleanupSelectedConfig',
'rest_framework',
'debug_toolbar',
'django_filters',
'store'
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'poizonstore.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'poizonstore.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False,
'DATETIME_FORMAT': '%d.%m.%Y %H:%M:%S',
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'utils.permissions.CsrfExemptSessionAuthentication',
),
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_PAGINATION_CLASS': 'utils.drf.StandardResultsSetPagination'
}
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'ru-RU'
TIME_ZONE = 'Europe/Moscow'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATIC_ROOT = os.path.join(BASE_DIR, 'assets')
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CHECKLIST_ID_LENGTH = 10
COMMISSION_OVER_150K = 1.1

29
poizonstore/urls.py Normal file
View File

@ -0,0 +1,29 @@
"""
URL configuration for poizonstore project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('__debug__/', include('debug_toolbar.urls')),
path('', include('store.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL)

16
poizonstore/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for poizonstore project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
application = get_wsgi_application()

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
# Core
Django==4.2.2
django-filter==23.2
djangorestframework==3.14.0
Pillow==9.5.0
# Misc
tqdm==4.65.0

Binary file not shown.

0
store/__init__.py Normal file
View File

50
store/admin.py Normal file
View File

@ -0,0 +1,50 @@
from django.contrib import admin
from .models import Category, Checklist, GlobalSettings, PaymentMethod, PromoCode, User, Image
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ('email', 'job_title', 'full_name',)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('slug', 'name')
ordering = ('id',)
@admin.register(Image)
class ImageAdmin(admin.ModelAdmin):
pass
@admin.register(Checklist)
class ChecklistAdmin(admin.ModelAdmin):
list_display = ('id', 'date', 'price_rub', 'commission_rub')
filter_horizontal = ('images',)
def date(self, obj: Checklist):
return obj.status_updated_at or obj.created_at
def get_queryset(self, request):
return Checklist.objects.with_base_related().annotate_price_rub().annotate_commission_rub()
@admin.register(GlobalSettings)
class GlobalSettingsAdmin(admin.ModelAdmin):
pass
@admin.register(PaymentMethod)
class PaymentMethodAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
@admin.register(PromoCode)
class PromoCodeAdmin(admin.ModelAdmin):
list_display = ('name', 'discount', 'free_delivery', 'no_comission')

6
store/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class StoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'store'

19
store/exceptions.py Normal file
View File

@ -0,0 +1,19 @@
from rest_framework import status
from rest_framework.exceptions import APIException
class CRMException(APIException):
def __init__(self, detail=None):
if detail is None:
detail = self.default_detail
self.detail = {'error': detail}
class AuthErrorMixin(CRMException):
"""Authentication exception error mixin."""
status_code = status.HTTP_401_UNAUTHORIZED
class InvalidCredentialsException(AuthErrorMixin):
default_detail = 'cannot find the worker'

View File

View File

View File

@ -0,0 +1,54 @@
from django.core.management import BaseCommand
from tqdm import tqdm
from store.models import Category, PaymentMethod
category_names = {
"shoes": "Обувь",
"outerwear": "Верхняя одежда",
"underwear": "Нижнее белье",
"bags": "Сумки",
"cosmetics": "Косметика",
"accessories": "Аксессуары",
"technics": "Техника",
"watches": "Часы",
"toys": "Игрушки",
"home": "Товары для дома",
"foodndrinks": "Еда и напитки",
"different": "Другое",
}
payment_methods = {
"alfa": {
"name": "Альфабанк",
"cardnumber": "",
"requisites": ""
},
"ralf": {
"name": "Райффайзен Банк",
"cardnumber": "",
"requisites": ""
},
"tink": {
"name": "Тинькофф",
"cardnumber": "",
"requisites": ""
}
}
class Command(BaseCommand):
help = ''' Create root categories '''
def create_categories(self):
for slug, name in tqdm(category_names.items(), desc="Creating categories"):
Category.objects.get_or_create(slug=slug, defaults={"name": name})
def create_payment_types(self):
for slug, data in tqdm(payment_methods.items(), desc="Creating payment methods"):
PaymentMethod.objects.get_or_create(slug=slug, defaults=data)
def handle(self, *args, **kwargs):
self.create_categories()
self.create_payment_types()

View File

@ -0,0 +1,107 @@
# Generated by Django 4.2.2 on 2023-06-30 22:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import store.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Эл. почта')),
('first_name', models.CharField(blank=True, max_length=150, null=True, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, null=True, verbose_name='last name')),
('middle_name', models.CharField(blank=True, max_length=150, null=True, verbose_name='Отчество')),
('job_title', models.CharField(choices=[('admin', 'Администратор'), ('ordermanager', 'Менеджер по заказам'), ('productmanager', 'Менеджер по закупкам')], max_length=30, verbose_name='Должность')),
('manager_id', models.CharField(blank=True, max_length=5, null=True, verbose_name='ID менеджера')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', store.models.UserManager()),
],
),
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='Название')),
('slug', models.SlugField(verbose_name='Идентификатор')),
],
),
migrations.CreateModel(
name='GlobalSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yuan_rate', models.DecimalField(decimal_places=2, max_digits=10)),
('delivery_price_CN', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('delivery_price_CN_RU', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('commission_rub', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
],
),
migrations.CreateModel(
name='PromoCode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название')),
('discount', models.PositiveSmallIntegerField(verbose_name='Скидка')),
('free_delivery', models.BooleanField(default=False, verbose_name='Бесплатная доставка')),
('no_comission', models.BooleanField(default=False, verbose_name='Без комиссии')),
],
),
migrations.CreateModel(
name='Checklist',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('status_updated_at', models.DateTimeField()),
('status', models.CharField(choices=[('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('completed', 'Завершен')], max_length=15, verbose_name='Статус заказа')),
('product_link', models.URLField(blank=True, null=True)),
('subcategory', models.CharField(blank=True, max_length=20, null=True, verbose_name='Подкатегория')),
('brand', models.CharField(blank=True, max_length=100, null=True, verbose_name='Бренд')),
('model', models.CharField(blank=True, max_length=100, null=True, verbose_name='Модель')),
('size', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Размер')),
('image', models.ImageField(blank=True, null=True, upload_to='')),
('preview_image', models.ImageField(blank=True, null=True, upload_to='')),
('price_yuan', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('comission', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('real_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('promocode', models.CharField(blank=True, max_length=100, null=True, verbose_name='Промокод')),
('comment', models.CharField(blank=True, max_length=200, null=True, verbose_name='Комментарий')),
('buyer_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Имя покупателя')),
('buyer_phone', models.CharField(blank=True, max_length=100, null=True, verbose_name='Телефон покупателя')),
('buyer_telegram', models.CharField(blank=True, max_length=100, null=True, verbose_name='Telegram покупателя')),
('receiver_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Имя получателя')),
('receiver_phone', models.CharField(blank=True, max_length=100, null=True, verbose_name='Телефон получателя')),
('payment_type', models.CharField(blank=True, choices=[('alfa', 'Альфа-Банк'), ('tink', 'Тинькофф Банк'), ('raif', 'Райффайзен Банк')], max_length=10, null=True, verbose_name='Метод оплаты')),
('payment_proof', models.ImageField(blank=True, null=True, upload_to='', verbose_name='Подтверждение оплаты')),
('cheque_photo', models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото чека')),
('delivery', models.CharField(blank=True, choices=[('pickup', 'Самовывоз из шоурума'), ('cdek', 'Пункт выдачи заказов CDEK')], max_length=10, null=True, verbose_name='Тип доставки')),
('track_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Трек-номер')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.category', verbose_name='Категория')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.2 on 2023-06-30 22:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='category',
options={'verbose_name': 'Category', 'verbose_name_plural': 'Categories'},
),
migrations.RemoveField(
model_name='checklist',
name='comission',
),
migrations.AlterField(
model_name='checklist',
name='price_yuan',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена в юанях'),
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 4.2.2 on 2023-07-01 16:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('store', '0002_alter_category_options_remove_checklist_comission_and_more'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='id',
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='checklist',
name='manager',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Менеджер'),
),
migrations.AlterField(
model_name='checklist',
name='product_link',
field=models.URLField(blank=True, null=True, verbose_name='Ссылка на товар'),
),
migrations.AlterField(
model_name='checklist',
name='real_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Реальная цена'),
),
migrations.AlterField(
model_name='checklist',
name='status_updated_at',
field=models.DateTimeField(verbose_name='Дата обновления статуса заказа'),
),
migrations.AlterField(
model_name='globalsettings',
name='delivery_price_CN',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='globalsettings',
name='delivery_price_CN_RU',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.2 on 2023-07-01 17:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0003_alter_checklist_id_alter_checklist_manager_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='globalsettings',
options={'verbose_name': 'GlobalSettings', 'verbose_name_plural': 'GlobalSettings'},
),
migrations.AlterField(
model_name='globalsettings',
name='yuan_rate',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 17:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0004_alter_globalsettings_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='globalsettings',
name='delivery_price_CN',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 17:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0005_alter_globalsettings_delivery_price_cn'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='price_yuan',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена в юанях'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0006_alter_checklist_price_yuan'),
]
operations = [
migrations.AlterField(
model_name='category',
name='slug',
field=models.SlugField(unique=True, verbose_name='Идентификатор'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-01 17:33
from django.db import migrations, models
import store.models
class Migration(migrations.Migration):
dependencies = [
('store', '0007_alter_category_slug'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='id',
field=models.CharField(default=store.models.generate_checklist_id, editable=False, max_length=10, primary_key=True, serialize=False),
),
]

View File

385
store/models.py Normal file
View File

@ -0,0 +1,385 @@
import time
from decimal import Decimal
from datetime import datetime
import random
import string
import uuid
from io import BytesIO
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.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):
cached = None
# 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.cached = self
def __str__(self) -> str:
return f'GlobalSettings for {self.id}'
@classmethod
def load(cls) -> 'GlobalSettings':
if cls.cached is not None:
return cls.cached
obj, _ = cls.objects.get_or_create(id=1)
cls.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)
manager_id = models.CharField("ID менеджера", max_length=5, blank=True, null=True)
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 PromoCode(models.Model):
name = models.CharField('Название', max_length=100, unique=True)
discount = models.DecimalField('Скидка', max_digits=10, decimal_places=2,)
free_delivery = models.BooleanField('Бесплатная доставка', default=False) # freedelivery
no_comission = models.BooleanField('Без комиссии', default=False) # nocomission
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
class Image(models.Model):
image = models.ImageField(upload_to='checklist_images')
is_preview = models.BooleanField(default=False)
class Meta:
verbose_name = 'Изображение'
verbose_name_plural = 'Изображения'
def __str__(self):
return getattr(self.image, 'name')
def generate_checklist_id():
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')\
.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"
CDEK = "cdek"
COMPLETED = "completed"
CHOICES = (
(DRAFT, 'Черновик'),
(NEW, 'Новый заказ'),
(PAYMENT, 'Проверка оплаты'),
(BUYING, 'На закупке'),
(BOUGHT, 'Закуплен'),
(CHINA, 'На складе в Китае'),
(CHINA_RUSSIA, 'Доставка на склад РФ'),
(RUSSIA, 'На складе в РФ'),
(CDEK, 'Доставляется СДЭК'),
(COMPLETED, 'Завершен'),
)
# Payment types
class PaymentType:
ALFA = "alfa"
TINKOFF = "tink"
RAIFFEISEN = "raif"
CHOICES = (
(ALFA, 'Альфа-Банк'),
(TINKOFF, 'Тинькофф Банк'),
(RAIFFEISEN, 'Райффайзен Банк'),
)
# Delivery
class DeliveryType:
PICKUP = "pickup"
CDEK = "cdek"
CHOICES = (
(PICKUP, 'Самовывоз из шоурума'),
(CDEK, 'Пункт выдачи заказов CDEK'),
)
created_at = models.DateTimeField(auto_now_add=True)
status_updated_at = models.DateTimeField('Дата обновления статуса заказа')
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)
images = models.ManyToManyField('Image', verbose_name='Картинки', blank=True)
# curencycurency2
price_yuan = models.DecimalField('Цена в юанях', max_digits=10, decimal_places=2, default=0)
# TODO: replace by parser
real_price = models.DecimalField('Реальная цена', max_digits=10, decimal_places=2, null=True, blank=True)
# TODO: choose from PromoCode table
# promo
promocode = models.CharField('Промокод', max_length=100, 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)
# paymenttype
payment_method = models.ForeignKey('PaymentMethod', verbose_name='Метод оплаты',
null=True, blank=True,
on_delete=models.SET_NULL)
payment_proof = models.ImageField('Подтверждение оплаты', null=True, blank=True) # paymentproovement
cheque_photo = models.ImageField('Фото чека', null=True, blank=True) # checkphoto
delivery = models.CharField('Тип доставки', max_length=10, choices=DeliveryType.CHOICES, null=True, blank=True)
# trackid
track_number = models.CharField('Трек-номер', max_length=100, 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:
return self.price_rub + GlobalSettings.load().delivery_price_CN + self.delivery_price_CN_RU
@property
def delivery_price_CN_RU(self) -> Decimal:
return getattr(self.category, 'delivery_price_CN_RU', 0.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.get(id=self.id)
# If status was updated, update status_updated_at field
if self.status != old_obj.status:
self.status_updated_at = timezone.now()
# 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)
image_obj.image.save(name=f'{self.id}_preview.jpg',
content=ContentFile(image_io.getvalue()),
save=True)
self.images.add(image_obj)
super().save(*args, **kwargs)

175
store/serializers.py Normal file
View File

@ -0,0 +1,175 @@
from django.contrib.auth import authenticate
from rest_framework import serializers
from store.exceptions import CRMException, InvalidCredentialsException
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, PromoCode, Image
class LoginSerializer(serializers.Serializer):
login = serializers.CharField(write_only=True, required=False)
password = serializers.CharField(trim_whitespace=False, write_only=True, required=False)
def validate(self, attrs):
email = attrs.get('login')
password = attrs.get('password')
if not email or not password:
raise CRMException('login and password is required')
user = authenticate(request=self.context.get('request'),
email=email,
password=password)
# The authenticate call simply returns None for is_active=False
# users. (Assuming the default ModelBackend authentication
# backend.)
if not user:
raise InvalidCredentialsException()
attrs['user'] = user
return attrs
class UserSerializer(serializers.ModelSerializer):
login = serializers.CharField(source='email', required=False)
job = serializers.CharField(source='job_title', required=False)
name = serializers.CharField(source='first_name', required=False)
lastname = serializers.CharField(source='middle_name', required=False)
surname = serializers.CharField(source='last_name', required=False)
managerid = serializers.CharField(source='manager_id', required=False)
class Meta:
model = User
fields = ('id', 'login', 'job', 'name', 'lastname', 'surname', 'managerid')
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ('image', )
class OrderImageListSerializer(serializers.ListSerializer):
child = ImageSerializer()
def to_representation(self, data):
images = super().to_representation(data)
return [image['image'] for image in images]
class ChecklistSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True)
managerid = serializers.CharField(source='manager.manager_id', required=False, allow_null=True)
link = serializers.URLField(source='product_link', required=False)
category = serializers.SlugRelatedField(slug_field='slug', queryset=Category.objects.all())
image = OrderImageListSerializer(source='main_images')
previewimage = serializers.ImageField(source='preview_image_url')
# TODO: choose from PromoCode table
promo = serializers.CharField(source='promocode', required=False)
currency = serializers.SerializerMethodField('get_yuan_rate')
curencycurency2 = serializers.DecimalField(source='price_yuan', max_digits=10, decimal_places=2)
currency3 = serializers.DecimalField(source='price_rub', max_digits=10, decimal_places=2, read_only=True)
chinadelivery = serializers.SerializerMethodField('get_delivery_price_CN')
chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', max_digits=10, decimal_places=2, read_only=True)
fullprice = serializers.DecimalField(source='full_price', max_digits=10, decimal_places=2)
realprice = serializers.DecimalField(source='real_price', max_digits=10, decimal_places=2)
comission = serializers.SerializerMethodField('get_comission')
buyername = serializers.CharField(source='buyer_name')
buyerphone = serializers.CharField(source='buyer_phone')
tg = serializers.CharField(source='buyer_telegram')
receivername = serializers.CharField(source='receiver_name')
reveiverphone = serializers.CharField(source='receiver_phone')
paymenttype = serializers.SlugRelatedField(source='payment_method', slug_field='slug', queryset=PaymentMethod.objects.all())
paymentproovement = serializers.ImageField(source='payment_proof')
checkphoto = serializers.ImageField(source='cheque_photo')
trackid = serializers.CharField(source='track_number')
delivery = serializers.CharField(source='get_delivery_display')
startDate = serializers.DateTimeField(source='created_at')
currentDate = serializers.DateTimeField(source='status_updated_at')
@staticmethod
def get_yuan_rate(obj: Checklist):
return GlobalSettings.load().yuan_rate
@staticmethod
def get_delivery_price_CN(obj: Checklist):
return GlobalSettings.load().delivery_price_CN
@staticmethod
def get_comission(obj: Checklist):
return GlobalSettings.load().commission_rub
class Meta:
model = Checklist
fields = ('id', 'status', 'managerid', 'link',
'category', 'subcategory',
'brand', 'model', 'size',
'image',
'previewimage',
'currency', 'curencycurency2', 'currency3', 'chinadelivery', 'chinadelivery2', 'comission',
'promo',
'comment',
'fullprice', 'realprice',
'buyername', 'buyerphone', 'tg',
'receivername', 'reveiverphone',
'paymenttype', 'paymentproovement', 'checkphoto',
'trackid', 'delivery',
'startDate', 'currentDate',
)
class GlobalSettingsYuanRateSerializer(serializers.ModelSerializer):
currency = serializers.DecimalField(source='yuan_rate', max_digits=10, decimal_places=2)
class Meta:
model = GlobalSettings
fields = ('currency',)
class GlobalSettingsPickupSerializer(serializers.ModelSerializer):
pickup = serializers.CharField(source='pickup_address')
class Meta:
model = GlobalSettings
fields = ('pickup',)
class GlobalSettingsPriceSerializer(serializers.ModelSerializer):
comission = 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 = ('comission', '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):
type = serializers.CharField(source='slug')
class Meta:
model = PaymentMethod
fields = ('type', 'name', 'requisites', 'cardnumber')
class PromocodeSerializer(serializers.ModelSerializer):
freedelivery = serializers.BooleanField(source='free_delivery')
nocomission = serializers.BooleanField(source='no_comission')
class Meta:
model = PromoCode
fields = ('name', 'discount', 'freedelivery', 'nocomission')

3
store/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

27
store/urls.py Normal file
View File

@ -0,0 +1,27 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from store import views
router = DefaultRouter()
# FIXME: renamed
router.register(r'statistics', views.StatisticsAPI, basename='statistics')
urlpatterns = [
path("login/", views.LoginAPI.as_view()),
path("users/", views.UserAPI.as_view()),
path("users/<int:pk>", views.UserAPI.as_view()),
path("checklist/", 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("pickup/", views.PickupAPI.as_view()),
path("category/price/", views.PricesAPI.as_view()),
path("payment/", views.PaymentMethodsAPI.as_view()),
path("promo/", views.PromoCodeAPI.as_view()),
] + router.urls

84
store/utils.py Normal file
View File

@ -0,0 +1,84 @@
import os
import textwrap
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
from poizonstore.settings import BASE_DIR
def create_preview(source_image: str, size=None, price_rub=None, title=None):
def get_font(font_size):
font_path = os.path.join(BASE_DIR, 'static', 'preview_image_font.ttf')
return ImageFont.truetype(font_path, size=font_size)
def resize_with_ar(image, size):
max_w, max_h = size
if image.height > image.width:
factor = max_h / image.height
else:
factor = max_w / image.width
return image.resize((int(image.width * factor), int(image.height * factor)))
if not os.path.isfile(source_image):
return None
# Create image
preview_width, preview_height = 800, 600
hor_padding = 15
vert_padding = 50
canvas_img = Image.new('RGBA', (preview_width, preview_height), color='white')
draw = ImageDraw.Draw(canvas_img)
# Draw top text
top_font = get_font(20)
text = 'Заказ в Poizon Store'
draw.text((hor_padding, vert_padding), text, font=top_font, fill='black')
# Draw title
if title:
title_font = get_font(40)
text = title
wrapped_text = textwrap.wrap(text, width=10)
x, start_y = hor_padding, vert_padding + 30
for line in wrapped_text:
draw.text((x, start_y), line, font=title_font, fill='black')
start_y += title_font.getbbox(line)[3]
# Draw size
if size:
size_text = str(size)
size_font = get_font(20)
x1, y1 = hor_padding, vert_padding + 30 + 100
x2, y2 = x1 + 40, y1 + 40
draw.rectangle((x1, y1, x2, y2), fill='black', width=2)
draw.text((x1 + 7, y1 + 7), size_text, font=size_font, fill='white')
# Draw price
if price_rub:
price_text = f"{str(price_rub)}"
price_font = get_font(50)
draw.text((hor_padding + 15, preview_height - 100), price_text, font=price_font, fill='black')
# Draw goods image
img2_box_w = preview_width - 270 - hor_padding * 2
img2_box_y = preview_height - vert_padding * 2
try:
with Image.open(source_image).convert("RGBA") as img2:
img2 = resize_with_ar(img2, (img2_box_w, img2_box_y))
img2_x = 270 + int(img2_box_w / 2) - int(img2.width / 2)
img2_y = 50 + int(img2_box_y / 2) - int(img2.height / 2)
canvas_img.paste(img2, (img2_x, img2_y), mask=img2)
return canvas_img.convert('RGB')
except UnidentifiedImageError:
return None
def concat_not_null_values(*values, separator=' '):
return separator.join([v for v in values if v is not None])

0
store/validators.py Normal file
View File

357
store/views.py Normal file
View File

@ -0,0 +1,357 @@
import calendar
import json
from collections import OrderedDict, defaultdict
from django.contrib.auth import login
from django.db.models import F, Count, Q, Sum
from django.utils import timezone
from rest_framework import generics, permissions, mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from store.exceptions import CRMException
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, PromoCode
from store.serializers import (UserSerializer, LoginSerializer, ChecklistSerializer, GlobalSettingsYuanRateSerializer,
CategorySerializer, GlobalSettingsPriceSerializer, PaymentMethodSerializer,
PromocodeSerializer, GlobalSettingsPickupSerializer)
class UserAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView):
serializer_class = UserSerializer
def get_queryset(self):
return User.objects.all()
def get(self, request, *args, **kwargs):
if 'pk' in kwargs:
return self.retrieve(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
# Update some data on current user
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = self.request.user
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class LoginAPI(generics.GenericAPIView):
serializer_class = LoginSerializer
permission_classes = (permissions.AllowAny,)
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
login(request, user)
return Response(serializer.data)
class ChecklistAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, generics.GenericAPIView):
serializer_class = ChecklistSerializer
lookup_field = 'id'
filterset_fields = ['status', ]
search_fields = ['id', 'track_id', 'buyer_phone', 'full_price']
def get_queryset(self):
return Checklist.objects.all().with_base_related() \
.annotate_price_rub().annotate_commission_rub() \
.default_ordering()
def get_object(self):
obj: Checklist = super().get_object()
# 3 hours maximum in 'neworder' status -> move to drafts
if obj.status == Checklist.Status.NEW:
diff_hours = (timezone.now() - obj.status_updated_at).seconds / 3600
if diff_hours > 3:
obj.status = Checklist.Status.DRAFT
obj.save()
return obj
def get(self, request, *args, **kwargs):
if 'id' in kwargs:
return self.retrieve(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
# Update some data on current user
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = get_object_or_404(self.get_queryset(), id=self.kwargs['id'])
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class YuanRateAPI(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})
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = self.get_object()
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class CategoryAPI(generics.GenericAPIView):
serializer_class = CategorySerializer
def get_queryset(self):
return Category.objects.all()
def get(self, request, *args, **kwargs):
categories_qs = self.get_queryset()
global_settings = GlobalSettings.load()
return Response({
'categories': CategorySerializer(categories_qs, many=True).data,
'prices': GlobalSettingsPriceSerializer(global_settings).data,
})
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
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):
serializer_class = GlobalSettingsPriceSerializer
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = GlobalSettings.load()
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class PickupAPI(generics.GenericAPIView):
serializer_class = GlobalSettingsPickupSerializer
def get_object(self):
return GlobalSettings.load()
def get(self, request, *args, **kwargs):
instance = self.get_object()
return Response(self.get_serializer(instance).data)
def patch(self, request, *args, **kwargs):
data = json.loads(request.body)
instance = GlobalSettings.load()
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class PaymentMethodsAPI(generics.GenericAPIView):
serializer_class = PaymentMethodSerializer
def get_queryset(self):
return PaymentMethod.objects.all()
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
data = {}
for obj in qs:
data[obj.slug] = self.get_serializer(obj).data
return Response(data)
# FIXME: use PATCH method for updates
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
if 'type' not in data:
raise CRMException('type is required')
instance = get_object_or_404(self.get_queryset(), slug=data['type'])
serializer = self.get_serializer(instance, data=data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class PromoCodeAPI(mixins.CreateModelMixin, generics.GenericAPIView):
serializer_class = PromocodeSerializer
lookup_field = 'name'
def get_queryset(self):
return PromoCode.objects.all()
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
return Response(
{'promo': self.get_serializer(qs, many=True).data}
)
def post(self, request, *args, **kwargs):
self.create(request, *args, **kwargs)
return self.get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
data = json.loads(request.body)
if 'name' not in data:
raise CRMException('name is required')
instance = get_object_or_404(self.get_queryset(), name=data['name'])
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class StatisticsAPI(viewsets.GenericViewSet):
def get_queryset(self):
return Checklist.objects.all() \
.filter(status=Checklist.Status.COMPLETED) \
.annotate(month=F('status_updated_at__month'))
@action(url_path='orders', detail=False, methods=['get'])
def stats_by_orders(self, request, *args, **kwargs):
global_settings = GlobalSettings.load()
yuan_rate = global_settings.yuan_rate
completed_filter = Q(status=Checklist.Status.COMPLETED)
# Prepare query to collect the stats
qs = self.get_queryset() \
.annotate_price_rub() \
.annotate_commission_rub() \
.values('month') \
.annotate(total=Count('id'),
total_completed=Count('id'),
total_bought_yuan=Sum('real_price', default=0),
total_bought_rub=F('total_bought_yuan') * yuan_rate,
total_commission_rub=Sum('_commission_rub', default=0)
)
def _create_stats(data: dict):
return {
"CountOrders": data.get('total', 0),
"CountComplete": data.get('total_completed', 0),
"SumCommission": data.get('total_commission_rub', 0),
"SumOrders1": data.get('total_bought_yuan', 0),
"SumOrders2": data.get('total_bought_rub', 0),
}
result = {}
# Add empty stats
for i in range(1, 13):
month_name = calendar.month_name[i]
result[month_name] = _create_stats(dict())
# Add actual stats
for stat in qs:
month_name = calendar.month_name[stat['month']]
result[month_name] = _create_stats(stat)
return Response(result)
@action(url_path='categories', detail=False, methods=['get'])
def stats_by_categories(self, request, *args, **kwargs):
all_categories = Category.objects.values_list('slug', flat=True)
categories_dict = {c: 0 for c in all_categories}
qs = self.get_queryset() \
.select_related('category') \
.values('month', 'category__slug') \
.annotate(total_orders=Count('id'))
result = {}
# Add empty stats
for i in range(1, 13):
month = calendar.month_name[i]
result[month] = categories_dict.copy()
# Add actual stats
for stat in qs:
month = calendar.month_name[stat['month']]
category_slug = stat['category__slug']
total_orders = stat['total_orders']
result[month][category_slug] = total_orders
return Response(result)
# TODO: implement stats_by_clients
@action(url_path='clients', detail=False, methods=['get'])
def stats_by_clients(self, request, *args, **kwargs):
def _create_stats(data: dict):
return {
"moreone": data.get('moreone', 0),
"moretwo": data.get('moretwo', 0),
"morethree": data.get('morethree', 0),
"morefour": data.get('morefour', 0),
"morefive": data.get('morefive', 0),
"moreten": data.get('moreten', 0),
"moretwentyfive": data.get('moretwentyfive', 0),
"morefifty": data.get('morefifty', 0),
}
def _filter_for_count(count):
return Count('buyer_phone',
filter=Q(buyer_phone__in=Checklist.objects.values('buyer_phone')
.annotate(total_orders=Count('id'))
.filter(total_orders__gt=count)
.values('buyer_phone')
))
qs = self.get_queryset() \
.values('month') \
.annotate(
moreone=_filter_for_count(1),
moretwo=_filter_for_count(2),
morethree=_filter_for_count(3),
morefour=_filter_for_count(4),
morefive=_filter_for_count(5),
moreten=_filter_for_count(10),
moretwentyfive=_filter_for_count(25),
morefifty=_filter_for_count(50),
)
result = {}
# Add empty stats
for i in range(1, 13):
month = calendar.month_name[i]
result[month] = _create_stats(dict())
# Add actual stats
for stat in qs:
month = calendar.month_name[stat['month']]
result[month] = _create_stats(stat)
return Response(result)

0
utils/__init__.py Normal file
View File

17
utils/drf.py Normal file
View File

@ -0,0 +1,17 @@
from collections import OrderedDict
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class StandardResultsSetPagination(PageNumberPagination):
page_size_query_param = 'limit'
max_page_size = 1000
def get_paginated_response(self, data):
return Response(OrderedDict([
('total_pages', self.page.paginator.count),
('limit', self.page.paginator.per_page),
('page', self.page.number),
('data', data),
]))

8
utils/permissions.py Normal file
View File

@ -0,0 +1,8 @@
from rest_framework import permissions
from rest_framework.authentication import SessionAuthentication
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
# To not perform the csrf check previously happening
return