Initial commit
This commit is contained in:
commit
1d615b2735
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
__pycache__/
|
||||
|
||||
# Static stuff
|
||||
!assets
|
||||
!media
|
||||
media/**/*
|
||||
assets/**/*
|
||||
|
||||
env
|
||||
.idea
|
||||
.DS_Store
|
||||
db.sqlite3
|
||||
BIN
_docs/cdekapi.pdf
Normal file
BIN
_docs/cdekapi.pdf
Normal file
Binary file not shown.
BIN
_docs/Доработки.pdf
Normal file
BIN
_docs/Доработки.pdf
Normal file
Binary file not shown.
BIN
_docs/ТЗ Rest Api.pdf
Normal file
BIN
_docs/ТЗ Rest Api.pdf
Normal file
Binary file not shown.
BIN
_docs/обновления исправления.pdf
Normal file
BIN
_docs/обновления исправления.pdf
Normal file
Binary file not shown.
0
cdek/__init__.py
Normal file
0
cdek/__init__.py
Normal file
89
cdek/api.py
Normal file
89
cdek/api.py
Normal 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
22
manage.py
Executable 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
0
poizonstore/__init__.py
Normal file
16
poizonstore/asgi.py
Normal file
16
poizonstore/asgi.py
Normal 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
163
poizonstore/settings.py
Normal 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
29
poizonstore/urls.py
Normal 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
16
poizonstore/wsgi.py
Normal 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
8
requirements.txt
Normal 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
|
||||
BIN
static/preview_image_font.ttf
Normal file
BIN
static/preview_image_font.ttf
Normal file
Binary file not shown.
0
store/__init__.py
Normal file
0
store/__init__.py
Normal file
50
store/admin.py
Normal file
50
store/admin.py
Normal 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
6
store/apps.py
Normal 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
19
store/exceptions.py
Normal 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'
|
||||
0
store/management/__init__.py
Normal file
0
store/management/__init__.py
Normal file
0
store/management/commands/__init__.py
Normal file
0
store/management/commands/__init__.py
Normal file
54
store/management/commands/create_initial_data.py
Normal file
54
store/management/commands/create_initial_data.py
Normal 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()
|
||||
|
||||
107
store/migrations/0001_initial.py
Normal file
107
store/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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='Цена в юанях'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
18
store/migrations/0006_alter_checklist_price_yuan.py
Normal file
18
store/migrations/0006_alter_checklist_price_yuan.py
Normal 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='Цена в юанях'),
|
||||
),
|
||||
]
|
||||
18
store/migrations/0007_alter_category_slug.py
Normal file
18
store/migrations/0007_alter_category_slug.py
Normal 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='Идентификатор'),
|
||||
),
|
||||
]
|
||||
19
store/migrations/0008_alter_checklist_id.py
Normal file
19
store/migrations/0008_alter_checklist_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
store/migrations/__init__.py
Normal file
0
store/migrations/__init__.py
Normal file
385
store/models.py
Normal file
385
store/models.py
Normal 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
175
store/serializers.py
Normal 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
3
store/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
27
store/urls.py
Normal file
27
store/urls.py
Normal 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
84
store/utils.py
Normal 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
0
store/validators.py
Normal file
357
store/views.py
Normal file
357
store/views.py
Normal 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
0
utils/__init__.py
Normal file
17
utils/drf.py
Normal file
17
utils/drf.py
Normal 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
8
utils/permissions.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user