Compare commits

..

No commits in common. "ef40e9f7e0ea991614e8bd7c31987c01f6589eb2" and "f5afc4061c0bf2961908fe3ff1e7709a7625b898" have entirely different histories.

121 changed files with 1608 additions and 3048 deletions

19
.env
View File

@ -1,18 +1 @@
APP_HOME=/var/www/poizonstore-stage
# === Keys ===
# Django
SECRET_KEY=""
ALLOWED_HOSTS=.crm-poizonstore.ru,127.0.0.1,localhost,45.84.227.72
# Telegram bot
TG_BOT_TOKEN=""
# External API settings
CDEK_CLIENT_ID=""
CDEK_CLIENT_SECRET=""
POIZON_TOKEN=""
CURRENCY_GETGEOIP_API_KEY=""
# Let's Encrypt
LETSENCRYPT_EMAIL="phzhitnikov@gmail.com"
APP_HOME=/var/www/phzhik-poizonstore/

1
.gitignore vendored
View File

@ -7,7 +7,6 @@ media/**/*
assets/**/*
env
*.env
.idea
.DS_Store
db.sqlite3

View File

@ -1,28 +0,0 @@
# Name of nodes to start
# here we have a single node
CELERYD_NODES="w1"
# or we could have three nodes:
#CELERYD_NODES="w1 w2 w3"
# Absolute or relative path to the 'celery' command:
CELERY_BIN="/var/www/poizonstore-stage/env/bin/celery"
# App instance to use
CELERY_APP="poizonstore"
# How to call manage.py
CELERYD_MULTI="multi"
# Extra command-line arguments to the worker
CELERYD_OPTS=""
# - %n will be replaced with the first part of the nodename.
# - %I will be replaced with the current child process index
# and is important when using the prefork pool to avoid race conditions.
CELERYD_PID_FILE="/var/run/celery-stage/%n.pid"
CELERYD_LOG_FILE="/var/log/celery-stage/%n%I.log"
CELERYD_LOG_LEVEL="INFO"
# you may wish to add these options for Celery Beat
CELERYBEAT_PID_FILE="/var/run/celery-stage/beat.pid"
CELERYBEAT_LOG_FILE="/var/log/celery-stage/beat.log"

View File

@ -1,25 +0,0 @@
[Unit]
Description=Celery Service
After=network.target
Requires=redis.service
[Service]
Type=forking
User=poizon
Group=poizon
EnvironmentFile=/etc/default/celery-stage
WorkingDirectory=/var/www/poizonstore-stage
RuntimeDirectory=celery
ExecStart=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi start $CELERYD_NODES \
--pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait $CELERYD_NODES \
--pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}"'
ExecReload=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi restart $CELERYD_NODES \
--pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -1,18 +0,0 @@
[Unit]
Description=Celery Beat Service
After=network.target
[Service]
Type=simple
User=poizon
Group=poizon
EnvironmentFile=/etc/default/celery-stage
WorkingDirectory=/var/www/poizonstore-stage
RuntimeDirectory=celery
ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
--pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -1,27 +1,21 @@
upstream django {
server 127.0.0.1:8002;
server 127.0.0.1:8001;
}
server {
listen 80;
server_name stage.crm-poizonstore.ru;
server_name crm-poizonstore.ru;
return 301 https://$host$request_uri;
}
server {
set $DOMAIN crm-poizonstore.ru;
set $APP_HOME /var/www/poizonstore-stage;
set $APP_HOME /var/www/phzhik-poizonstore;
listen 443 ssl;
server_name $DOMAIN;
server_name crm-poizonstore.ru;
charset utf-8;
# === Add here SSL config ===
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# max upload size
client_max_body_size 75M; # adjust to taste
@ -46,8 +40,4 @@ server {
include /etc/nginx/uwsgi_params;
include /etc/nginx/proxy_params;
}
location /flower/ {
proxy_pass http://localhost:5555/flower/;
}
}

View File

@ -1,14 +0,0 @@
#!/bin/sh
WORK_DIR="/var/www/poizonstore"
CELERY_BIN="/var/www/poizonstore/env/bin"
PROJECT_NAME="poizonstore"
# Wait for worker to start
until timeout 10s $CELERY_BIN/celery -A $PROJECT_NAME --workdir=$WORK_DIR inspect ping; do
>&2 echo "Celery workers not available"
done
# Run flower for Celery management
echo 'Starting Celery flower'
$CELERY_BIN/celery -A $PROJECT_NAME --workdir=$WORK_DIR flower --port=5555 --url_prefix=/flower --basic-auth=admin:meowmeow

View File

@ -1,28 +1,25 @@
[uwsgi]
project = poizonstore-stage
uid = poizon
gid = poizon
# Django-related settings
# the base directory (full path)
chdir = /var/www/%(project)/
chdir = /var/www/phzhik-poizonstore/
# Django's wsgi file
module = poizonstore:application
module = vba_portal:application
# the virtualenv (full path)
virtualenv = /var/www/%(project)/env
virtualenv = /var/www/phzhik-poizonstore/env
# process-related settings
# master
master = true
# maximum number of worker processes
processes = 10
processes = 1
# the socket (use the full path to be safe
#socket = /var/www/%(project)/mysite.sock
#socket = /var/www/phzhik-poizonstore/mysite.sock
socket = :8001
wsgi-file = /var/www/%(project)/poizonstore/wsgi.py
pidfile = /tmp/uwsgi-%(project).pid
stats = /tmp/uwsgi.stats.sock
wsgi-file = /var/www/phzhik-poizonstore/poizonstore/wsgi.py
pidfile = /tmp/uwsgi.pid
# ... with appropriate permissions - may be needed
chmod-socket = 664
# clear environment on exit

View File

@ -1,21 +0,0 @@
from django.contrib import admin
from .models import User, BonusProgramTransaction
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ('email', 'role', 'full_name', 'phone', 'telegram', 'balance')
def get_queryset(self, request):
return User.objects.with_base_related()
@admin.register(BonusProgramTransaction)
class BonusProgramTransactionAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'user', 'date', 'amount', 'comment', 'order', 'was_cancelled')
def delete_queryset(self, request, queryset):
for obj in queryset:
obj.cancel_transaction()

View File

@ -1,9 +0,0 @@
from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'
def ready(self):
import account.signals

View File

@ -1,5 +0,0 @@
from rest_framework import exceptions, status
class AuthError(exceptions.APIException):
status_code = status.HTTP_401_UNAUTHORIZED

View File

@ -1,71 +0,0 @@
# Generated by Django 4.2.2 on 2024-03-28 22:05
import account.models
from django.conf import settings
from django.db import migrations, models
import django.utils.timezone
import account.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
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='Отчество')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('ordermanager', 'Менеджер по заказам'), ('productmanager', 'Менеджер по закупкам'), ('client', 'Клиент')], default='client', max_length=30, verbose_name='Роль')),
('phone', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='Телефон')),
('telegram', models.CharField(blank=True, max_length=100, null=True, verbose_name='Telegram')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', account.models.UserManager()),
],
),
migrations.CreateModel(
name='BonusProgramTransaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(0, 'Другое начисление'), (1, 'Бонус за регистрацию'), (2, 'Бонус за покупку'), (3, 'Бонус за первую покупку приглашенного'), (4, 'Бонус за первую покупку'), (10, 'Другое списание'), (11, 'Списание бонусов за заказ')], verbose_name='Тип транзакции')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата транзакции')),
('amount', models.SmallIntegerField(verbose_name='Количество, руб')),
],
),
migrations.CreateModel(
name='ReferralRelationship',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invited', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_invited', to=settings.AUTH_USER_MODEL)),
('inviter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_inviter', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='BonusProgramUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('balance', models.PositiveSmallIntegerField(default=0, verbose_name='Баланс, руб')),
('referral_code', models.CharField(default=account.models.generate_referral_code, editable=False, max_length=9)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,42 +0,0 @@
# Generated by Django 4.2.2 on 2024-03-28 22:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('store', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('account', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bonusprogramtransaction',
name='order',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.checklist'),
),
migrations.AddField(
model_name='bonusprogramtransaction',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.bonusprogramuser', verbose_name='Пользователь транзакции'),
),
migrations.AddField(
model_name='user',
name='groups',
field=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'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=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'),
),
migrations.AlterUniqueTogether(
name='referralrelationship',
unique_together={('inviter', 'invited')},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-03 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0002_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-03 21:26
from django.db import migrations
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('account', '0003_alter_user_email'),
]
operations = [
migrations.AlterField(
model_name='user',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True, verbose_name='Телефон'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-03 21:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0004_alter_user_phone'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='Эл. почта'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-05 21:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0005_alter_user_email'),
]
operations = [
migrations.AddField(
model_name='user',
name='tg_user_id',
field=models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='id пользователя в Telegram'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-07 17:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0007_user_is_draft_user'),
]
operations = [
migrations.AlterField(
model_name='bonusprogramtransaction',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь транзакции'),
),
migrations.DeleteModel(
name='BonusProgramUser',
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-07 17:36
import account.models
from django.db import migrations, models
import account.models
class Migration(migrations.Migration):
dependencies = [
('account', '0008_alter_bonusprogramtransaction_user_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={},
),
migrations.AddField(
model_name='user',
name='balance',
field=models.PositiveSmallIntegerField(default=0, verbose_name='Баланс, руб'),
),
migrations.AddField(
model_name='user',
name='referral_code',
field=models.CharField(default=account.models.generate_referral_code, editable=False, max_length=10),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-07 20:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0009_alter_user_options_user_balance_user_referral_code'),
]
operations = [
migrations.AddField(
model_name='bonusprogramtransaction',
name='comment',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Комментарий'),
),
]

View File

@ -1,57 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-07 23:27
from django.contrib.auth.hashers import make_password
from django.db import migrations
from phonenumber_field.phonenumber import PhoneNumber
def move_buyer_info_to_account(apps, schema_editor):
Checklist = apps.get_model("store", "Checklist")
User = apps.get_model("account", "User")
# Normalize phone numbers first
for order in Checklist.objects.all():
if order.buyer_phone is None:
continue
old_phone = order.buyer_phone
new_phone = PhoneNumber.from_string(order.buyer_phone, region="RU").as_e164
if old_phone != new_phone:
print(f"{old_phone} -> {new_phone}")
order.buyer_phone = new_phone
order.save(update_fields=['buyer_phone'])
# Move buyer info to User
for order in Checklist.objects.all():
fields_to_copy = {
'first_name': order.buyer_name,
'telegram': order.buyer_telegram,
}
if order.buyer_phone is None:
User.objects.create(**fields_to_copy)
created = True
else:
obj, created = User.objects.update_or_create(phone=order.buyer_phone, defaults=fields_to_copy)
if created:
obj.is_draft_user = True
obj.password = make_password(None)
obj.save(update_fields=['password'])
# Bind customer to order
order.customer_id = obj.id
order.save(update_fields=['customer_id'])
class Migration(migrations.Migration):
dependencies = [
('account', '0010_bonusprogramtransaction_comment'),
]
operations = [
migrations.AlterModelOptions(
name='bonusprogramtransaction',
options={'ordering': ['-date']},
),
migrations.RunPython(move_buyer_info_to_account, migrations.RunPython.noop),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-08 02:59
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('account', '0011_alter_bonusprogramtransaction_options'),
]
operations = [
migrations.AddField(
model_name='referralrelationship',
name='invited_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-08 03:01
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0012_referralrelationship_invited_at'),
]
operations = [
migrations.AlterField(
model_name='referralrelationship',
name='invited',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_inviter', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='referralrelationship',
name='inviter',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_invited', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-14 14:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0013_alter_referralrelationship_invited_and_more'),
]
operations = [
migrations.AlterField(
model_name='user',
name='balance',
field=models.PositiveSmallIntegerField(default=0, editable=False, verbose_name='Баланс, руб'),
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-21 03:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('store', '0003_remove_checklist_buyer_name_and_more'),
('account', '0014_alter_user_balance'),
]
operations = [
migrations.AlterModelOptions(
name='bonusprogramtransaction',
options={'ordering': ['-date'], 'verbose_name': 'История баланса', 'verbose_name_plural': 'История баланса'},
),
migrations.AddField(
model_name='bonusprogramtransaction',
name='was_cancelled',
field=models.BooleanField(default=False, editable=False, verbose_name='Была отменена'),
),
migrations.AlterField(
model_name='bonusprogramtransaction',
name='order',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.checklist', verbose_name='Связанный заказ'),
),
migrations.AlterField(
model_name='bonusprogramtransaction',
name='type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Другое начисление'), (1, 'Бонус за регистрацию'), (2, 'Бонус за покупку'), (3, 'Бонус за первую покупку приглашенного'), (4, 'Бонус за первую покупку'), (10, 'Другое списание'), (11, 'Списание бонусов за заказ'), (20, 'Отмена начисления'), (21, 'Отмена списания')], verbose_name='Тип транзакции'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-23 09:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0015_alter_bonusprogramtransaction_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='user',
name='role',
field=models.CharField(choices=[('admin', 'Администратор'), ('ordermanager', 'Менеджер по заказам'), ('productmanager', 'Менеджер по закупкам'), ('client', 'Клиент')], default='client', editable=False, max_length=30, verbose_name='Роль'),
),
]

View File

@ -1,4 +0,0 @@
from .bonus import generate_referral_code, BonusType, BonusProgramMixin, BonusProgramTransaction, BonusProgramTransactionQuerySet
from .user import User, UserManager, UserQuerySet, ReferralRelationship

View File

@ -1,278 +0,0 @@
import logging
from contextlib import suppress
from django.conf import settings
from django.db import models
from django.db.models import Sum
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.formats import localize
from store.models import Checklist
from tg_bot.messages import TGBonusMessage
logger = logging.getLogger(__name__)
class BonusType:
# Другое начисление
OTHER_DEPOSIT = 0
# Клиент передал номер ТГ-боту
SIGNUP = 1
# Клиент сделал заказ
DEFAULT_PURCHASE = 2
# Приглашенный клиент сделал свою первую покупку, бонус реферреру
FOR_INVITER = 3
# Клиент сделал заказ и получил бонус за первую покупку от реферрера
INVITED_FIRST_PURCHASE = 4
# Другое списание
OTHER_WITHDRAWAL = 10
# Клиент потратил баллы на заказ
SPENT_PURCHASE = 11
# Отмена начисления
CANCELLED_DEPOSIT = 20
# Отмена списания
CANCELLED_WITHDRAWAL = 21
CHOICES = (
(OTHER_DEPOSIT, 'Другое начисление'),
(SIGNUP, 'Бонус за регистрацию'),
(DEFAULT_PURCHASE, 'Бонус за покупку'),
(FOR_INVITER, 'Бонус за первую покупку приглашенного'),
(INVITED_FIRST_PURCHASE, 'Бонус за первую покупку'),
(OTHER_WITHDRAWAL, 'Другое списание'),
(SPENT_PURCHASE, 'Списание бонусов за заказ'),
(CANCELLED_DEPOSIT, 'Отмена начисления'),
(CANCELLED_WITHDRAWAL, 'Отмена списания'),
)
class BonusProgramTransactionQuerySet(models.QuerySet):
# TODO: optimize queries
def with_base_related(self):
return self
class BonusProgramTransaction(models.Model):
""" Represents the history of all bonus program transactions """
type = models.PositiveSmallIntegerField('Тип транзакции', choices=BonusType.CHOICES)
user = models.ForeignKey('User', verbose_name='Пользователь транзакции', on_delete=models.CASCADE)
date = models.DateTimeField('Дата транзакции', auto_now_add=True)
amount = models.SmallIntegerField('Количество, руб')
comment = models.CharField('Комментарий', max_length=200, null=True, blank=True)
was_cancelled = models.BooleanField('Была отменена', editable=False, default=False)
# Bound objects
order = models.ForeignKey('store.Checklist', verbose_name="Связанный заказ", null=True, blank=True, on_delete=models.SET_NULL)
objects = BonusProgramTransactionQuerySet.as_manager()
class Meta:
ordering = ['-date']
verbose_name = "История баланса"
verbose_name_plural = "История баланса"
def _notify_user_about_new_transaction(self):
msg = None
match self.type:
case BonusType.SIGNUP:
msg = TGBonusMessage.SIGNUP.format(amount=self.amount)
case BonusType.DEFAULT_PURCHASE:
msg = TGBonusMessage.PURCHASE_ADDED.format(amount=self.amount, order_id=self.order.id)
case BonusType.FOR_INVITER:
msg = TGBonusMessage.FOR_INVITER.format(amount=self.amount)
case BonusType.INVITED_FIRST_PURCHASE:
msg = TGBonusMessage.INVITED_FIRST_PURCHASE.format(amount=self.amount, order_id=self.order.id)
case BonusType.OTHER_DEPOSIT | BonusType.CANCELLED_DEPOSIT:
comment = self.comment or ""
msg = TGBonusMessage.OTHER_DEPOSIT.format(amount=self.amount, comment=comment)
case BonusType.OTHER_WITHDRAWAL | BonusType.CANCELLED_WITHDRAWAL:
comment = self.comment or ""
msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment)
case BonusType.SPENT_PURCHASE:
msg = TGBonusMessage.PURCHASE_SPENT.format(amount=abs(self.amount), order_id=self.order_id)
case _:
pass
if msg is not None:
self.user.notify_tg_bot(msg)
def cancel_transaction(self):
# Skip already cancelled transactions
# TODO: if reverse transaction is being deleted, revert the source one?
if self.was_cancelled or self.type in (BonusType.OTHER_WITHDRAWAL, BonusType.OTHER_DEPOSIT):
return
date_formatted = localize(timezone.localtime(self.date))
if self.amount > 0:
comment = f"Отмена начисления #{self.id} от {date_formatted}"
bonus_type = BonusType.OTHER_WITHDRAWAL
elif self.amount < 0:
comment = f"Отмена списания #{self.id} от {date_formatted}"
bonus_type = BonusType.OTHER_DEPOSIT
else:
return
# Create reverse transaction, user's balance will be recalculated in post_save signal
transaction = BonusProgramTransaction()
transaction.user_id = self.user_id
transaction.type = bonus_type
transaction.amount = self.amount * -1
transaction.comment = comment
transaction.order = self.order
transaction.save()
self.was_cancelled = True
self.save()
def delete(self, *args, **kwargs):
# Don't delete transaction, cancel it instead
self.cancel_transaction()
def save(self, *args, **kwargs):
if self.id is None:
self._notify_user_about_new_transaction()
return super().save(*args, **kwargs)
def generate_referral_code():
""" Generate unique numeric referral code for User """
from account.models import User
while True:
allowed_chars = "0123456789"
code = get_random_string(settings.REFERRAL_CODE_LENGTH, allowed_chars)
# Hacky code for migrations
if "referral_code" not in User._meta.fields:
return code
if not User.objects.filter(referral_code=code).exists():
return code
class BonusProgramMixin(models.Model):
balance = models.PositiveSmallIntegerField('Баланс, руб', default=0, editable=False)
referral_code = models.CharField(max_length=settings.REFERRAL_CODE_LENGTH, default=generate_referral_code,
editable=False)
class Meta:
abstract = True
@property
def bonus_history(self):
return BonusProgramTransaction.objects.filter(user_id=self.id)
def update_balance(self, amount, bonus_type, comment=None, order=None):
# No underflow or dummy transactions allowed
if amount == 0 or (self.balance + amount) < 0:
return
# Create bonus transaction, user's balance will be recalculated in post_save signal
transaction = BonusProgramTransaction(user_id=self.id,
amount=amount, type=bonus_type,
comment=comment, order=order)
transaction.save()
def recalculate_balance(self):
# TODO: use this method when checking the available balance upon order creation
total_balance = BonusProgramTransaction.objects \
.filter(user_id=self.id) \
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
self.balance = max(0, total_balance)
self.save(update_fields=['balance'])
def add_signup_bonus(self):
bonus_type = BonusType.SIGNUP
amount = self._get_bonus_amount("signup")
already_exists = (BonusProgramTransaction.objects
.filter(user_id=self.id, type=bonus_type)
.exists())
if already_exists:
self._log(logging.INFO, "User already had signup bonus")
return
self.update_balance(amount, bonus_type)
def add_order_bonus(self, order):
from store.models import Checklist
bonus_type = BonusType.DEFAULT_PURCHASE
amount = self._get_bonus_amount("default_purchase")
if order.status != Checklist.Status.CHINA_RUSSIA:
return
already_exists = (BonusProgramTransaction.objects
.filter(user_id=self.id, type=bonus_type, order_id=order.id)
.exists())
if already_exists:
self._log(logging.INFO, f"User already got bonus for order #{order.id}")
return
self.update_balance(amount, bonus_type, order=order)
def add_referral_bonus(self, order: Checklist, for_inviter: bool):
amount = self._get_bonus_amount("referral")
# Check if data is sufficient
if order.customer_id is None or order.customer.inviter is None:
return
# Check if eligible
# Bonus is only for first purchase and only for orders that reached the CHINA_RUSSIA status
if order.status != Checklist.Status.CHINA_RUSSIA or order.customer.customer_orders.count() != 1:
return
user = order.customer.inviter if for_inviter else order.customer
bonus_type = BonusType.FOR_INVITER if for_inviter else BonusType.INVITED_FIRST_PURCHASE
# Check if user didn't receive bonus yet
already_exists = (BonusProgramTransaction.objects
.filter(user_id=user.id, type=bonus_type, order_id=order.id)
.exists())
if already_exists:
self._log(logging.INFO, f"User already got referral bonus for order #{order.id}")
return
# Add bonuses
user.update_balance(amount, bonus_type, order=order)
@staticmethod
def _get_bonus_amount(config_key) -> int:
amount = 0
with suppress(KeyError):
amount = settings.BONUS_PROGRAM_CONFIG["amounts"][config_key]
return amount
# TODO: move to custom logger
def _log(self, level, message: str):
message = f"[BonusProgram #{self.id}] {message}"
logger.log(level, message)

View File

@ -1,201 +0,0 @@
import logging
from asgiref.sync import sync_to_async
from django.contrib.admin import display
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager as _UserManager, AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from account.models import BonusProgramMixin
from store.utils import concat_not_null_values
from tg_bot.tasks import send_tg_message
logger = logging.getLogger(__name__)
class UserQuerySet(models.QuerySet):
# TODO: optimize queries
def with_base_related(self):
return self
class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
def _create_user(self, email, password, **extra_fields):
if email:
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_draft_user(self, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_draft_user", True)
return self._create_user(email=None, password=None, **extra_fields)
def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("role", 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)
def invite_user(self, referral_code, user_id):
inviter = User.objects.filter(referral_code=referral_code).first()
user_to_invite = User.objects.filter(id=user_id).first()
if inviter is None or user_to_invite is None:
return
if inviter.id == user_to_invite.id:
logger.warning(f"User #{inviter.id} tried to invite himself via referral code {referral_code}")
return
obj, created = ReferralRelationship.objects.get_or_create(inviter_id=inviter.id, invited_id=user_to_invite.id)
if not created:
logger.warning(f"User #{inviter.id} already invited user #{user_id}")
return
async def bind_tg_user(self, tg_user_id, phone, referral_code=None):
# Normalize phone: 79111234567 -> +79111234567
phone = PhoneNumber.from_string(phone).as_e164
"""
1) No user with given phone or tg_user_id -> create draft user, add tg_user_id & phone
2) User exists with given phone, but no tg_user_id -> add tg_user_id to User
3) User exists with tg_user_id, but no phone -> add phone to User
4) User exists with given tg_user_id & phone -> just authorize
"""
user = await User.objects.filter(
Q(phone=phone) | (Q(tg_user_id=tg_user_id))
).afirst()
freshly_created = False
# Sign up through Telegram bot
if user is None:
user = await sync_to_async(self.create_draft_user)(phone=phone, tg_user_id=tg_user_id)
logger.info(f"tgbot: Created draft user #{user.id} for phone [{phone}]")
freshly_created = True
# First-time binding Telegram <-> User ?
if freshly_created or user.tg_user_id is None:
# Add bonus for Telegram login
await sync_to_async(user.add_signup_bonus)()
# Create referral relationship
# Only for fresh registration
if freshly_created and referral_code is not None:
await sync_to_async(User.objects.invite_user)(referral_code, user.id)
# Bind Telegram chat to user
if not freshly_created:
user.phone = phone
user.tg_user_id = tg_user_id
await user.asave()
logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")
class User(BonusProgramMixin, AbstractUser):
ADMIN = "admin"
ORDER_MANAGER = "ordermanager"
PRODUCT_MANAGER = "productmanager"
CLIENT = "client"
ROLE_CHOICES = (
(ADMIN, 'Администратор'),
(ORDER_MANAGER, 'Менеджер по заказам'),
(PRODUCT_MANAGER, 'Менеджер по закупкам'),
(CLIENT, 'Клиент'),
)
# Login by email
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['phone']
username = None
email = models.EmailField("Эл. почта", blank=True, null=True, unique=True)
# Base info
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)
role = models.CharField("Роль", max_length=30, choices=ROLE_CHOICES, default=CLIENT, editable=False)
# Contacts
phone = PhoneNumberField('Телефон', null=True, blank=True, unique=True)
telegram = models.CharField('Telegram', max_length=100, null=True, blank=True)
# Bot-related
# User is created via Telegram bot and has no password yet.
# User can set initial password via /users/set_initial_password/
is_draft_user = models.BooleanField("Черновик пользователя", default=False)
tg_user_id = models.BigIntegerField("id пользователя в Telegram", null=True, blank=True, unique=True)
objects = UserManager()
def __str__(self):
value = self.email or self.phone or self.id
return str(value)
@property
def is_superuser(self):
return self.role == self.ADMIN
@property
def is_manager(self):
return self.role in (self.ADMIN, self.ORDER_MANAGER, self.PRODUCT_MANAGER)
@display(description='ФИО')
def full_name(self):
return concat_not_null_values(self.last_name, self.first_name, self.middle_name)
@property
def invited_users(self):
return User.objects.filter(user_inviter__inviter=self.id)
@property
def inviter(self):
return User.objects.filter(user_invited__invited=self.id).first()
def notify_tg_bot(self, message, **kwargs):
if self.tg_user_id is None:
return
send_tg_message.delay(self.tg_user_id, message, **kwargs)
def save(self, *args, **kwargs):
# If password changed, it is no longer a draft User
if self._password is not None:
self.is_draft_user = False
super().save(*args, **kwargs)
class ReferralRelationship(models.Model):
invited_at = models.DateTimeField(auto_now_add=True)
inviter = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_invited")
invited = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_inviter")
class Meta:
unique_together = (('inviter', 'invited'),)
def clean(self):
if self.inviter_id == self.invited_id:
raise ValidationError("User can't invite himself")

View File

@ -1,22 +0,0 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class ReadOnly(BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class IsClient(BasePermission):
def has_permission(self, request, view):
from account.models import User
return request.user.is_authenticated and request.user.role == User.CLIENT
class IsManager(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_manager
class IsAdmin(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_superuser

View File

@ -1,104 +0,0 @@
from django.conf import settings
from django.db.models import Q
from djoser import serializers as djoser_serializers
from djoser.conf import settings as djoser_settings
from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed
from .models import User, BonusProgramTransaction, BonusType
from .utils import verify_telegram_authentication
class UserSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='first_name')
lastname = serializers.CharField(source='middle_name')
surname = serializers.CharField(source='last_name')
class Meta:
model = User
fields = ('id', 'email', 'phone', 'role', 'name', 'lastname', 'surname', 'balance', 'referral_code', 'is_draft_user')
class BonusProgramTransactionSerializer(serializers.ModelSerializer):
type = serializers.CharField(source='get_type_display')
class Meta:
model = BonusProgramTransaction
fields = ('id', 'type', 'date', 'amount', 'comment', 'was_cancelled')
def non_zero_validator(value):
if value == 0:
raise serializers.ValidationError("Value cannot be zero")
return value
class UserBalanceUpdateSerializer(BonusProgramTransactionSerializer):
amount = serializers.IntegerField(validators=[non_zero_validator])
type = serializers.SerializerMethodField()
class Meta:
model = BonusProgramTransactionSerializer.Meta.model
fields = BonusProgramTransactionSerializer.Meta.fields
read_only_fields = ('id', 'type', 'date')
def get_type(self, instance):
# Deposit or spent depending on value
if instance['amount'] < 0:
return BonusType.OTHER_WITHDRAWAL
elif instance['amount'] > 0:
return BonusType.OTHER_DEPOSIT
class SetInitialPasswordSerializer(djoser_serializers.PasswordSerializer):
def validate(self, attrs):
user = getattr(self, "user", None) or self.context["request"].user
# why assert? There are ValidationError / fail everywhere
assert user is not None
if not user.is_superuser and not user.is_draft_user:
raise serializers.ValidationError("To change password, use /users/change_password endpoint")
return super().validate(attrs)
class UserCreateSerializer(djoser_serializers.UserCreateSerializer):
email = serializers.EmailField(required=True)
class TokenCreateSerializer(serializers.Serializer):
email_or_phone = serializers.CharField()
password = serializers.CharField(required=False, style={"input_type": "password"})
default_error_messages = {
"invalid_credentials": djoser_settings.CONSTANTS.messages.INVALID_CREDENTIALS_ERROR,
"inactive_account": djoser_settings.CONSTANTS.messages.INACTIVE_ACCOUNT_ERROR,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
def validate(self, attrs):
email_or_phone = attrs.get('email_or_phone')
password = attrs.get("password")
user = User.objects.filter(Q(email=email_or_phone) | Q(phone=email_or_phone)).first()
if not user or not user.check_password(password) or not user.is_active:
raise AuthenticationFailed()
self.user = user
return attrs
class TelegramCallbackSerializer(serializers.Serializer):
id = serializers.IntegerField()
first_name = serializers.CharField(allow_null=True)
username = serializers.CharField(allow_null=True)
photo_url = serializers.URLField(allow_null=True)
auth_date = serializers.IntegerField()
hash = serializers.CharField()
def validate(self, attrs):
verify_telegram_authentication(bot_token=settings.TG_BOT_TOKEN, request_data=attrs)
return attrs

View File

@ -1,29 +0,0 @@
import logging
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from account.models import User, ReferralRelationship, BonusProgramTransaction
logger = logging.getLogger(__name__)
@receiver(post_save, sender=User)
def handle_user_save(sender, instance: User, created, **kwargs):
pass
@receiver(post_save, sender=ReferralRelationship)
def handle_invitation_save(sender, instance: ReferralRelationship, created, **kwargs):
if created:
logger.info(f"User {instance.inviter_id} invited {instance.invited_id}")
# TODO: notify about invitation
@receiver(post_save, sender=BonusProgramTransaction)
@receiver(post_delete, sender=BonusProgramTransaction)
def handle_bonus_transaction_savedelete(sender, instance: BonusProgramTransaction, **kwargs):
# Recalculate user's balance
if instance.user is not None:
instance.user.recalculate_balance()

View File

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

View File

@ -1,13 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from account import views
router = DefaultRouter()
router.register("users", views.UserViewSet)
urlpatterns = [
path('', include(router.urls)),
path('auth/', include('djoser.urls.authtoken')),
path('auth/telegram/', views.TelegramLoginForm.as_view()),
]

View File

@ -1,57 +0,0 @@
import hashlib
import hmac
import time
class NotTelegramDataError(Exception):
""" The verification algorithm did not authorize Telegram data. """
pass
class TelegramDataIsOutdatedError(Exception):
""" The Telegram data is outdated. """
pass
# Source: https://github.com/dmytrostriletskyi/django-telegram-login/blob/master/django_telegram_login/authentication.py
def verify_telegram_authentication(bot_token, request_data):
"""
Check if received data from Telegram is real.
Based on SHA and HMAC algothims.
Instructions - https://core.telegram.org/widgets/login#checking-authorization
"""
ONE_DAY_IN_SECONDS = 86400
request_data = request_data.copy()
received_hash = request_data['hash']
auth_date = request_data['auth_date']
request_data.pop('hash', None)
request_data_alphabetical_order = sorted(request_data.items(), key=lambda x: x[0])
data_check_string = []
for data_pair in request_data_alphabetical_order:
key, value = data_pair[0], str(data_pair[1])
data_check_string.append(key + '=' + value)
data_check_string = '\n'.join(data_check_string)
secret_key = hashlib.sha256(bot_token.encode()).digest()
_hash = hmac.new(secret_key, msg=data_check_string.encode(), digestmod=hashlib.sha256).hexdigest()
unix_time_now = int(time.time())
unix_time_auth_date = int(auth_date)
if unix_time_now - unix_time_auth_date > ONE_DAY_IN_SECONDS:
raise TelegramDataIsOutdatedError(
'Authentication data is outdated. Authentication was received more than day ago.'
)
if _hash != received_hash:
raise NotTelegramDataError(
'This is not a Telegram data. Hash from recieved authentication data does not match'
'with calculated hash based on bot token.'
)

View File

@ -1,153 +0,0 @@
from django.conf import settings
from djoser import views as djoser_views
from djoser.conf import settings as djoser_settings
from djoser.permissions import CurrentUserOrAdmin
from djoser.utils import login_user
from rest_framework import views, status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound, ValidationError, MethodNotAllowed
from rest_framework.permissions import AllowAny
from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.response import Response
from account.models import User
from account.serializers import SetInitialPasswordSerializer, BonusProgramTransactionSerializer, \
UserBalanceUpdateSerializer, TelegramCallbackSerializer
from tg_bot.handlers.start import request_phone_sync
from tg_bot.messages import TGCoreMessage
from tg_bot.bot import bot_sync
class UserViewSet(djoser_views.UserViewSet):
""" Replacement for Djoser's UserViewSet """
def permission_denied(self, request, **kwargs):
if (
djoser_settings.HIDE_USERS
and request.user.is_authenticated
and self.action in ["balance"]
):
raise NotFound()
super().permission_denied(request, **kwargs)
def get_permissions(self):
if self.action == "set_initial_password":
self.permission_classes = djoser_settings.PERMISSIONS.set_password
return super().get_permissions()
def get_serializer_class(self):
if self.action == "set_initial_password":
return SetInitialPasswordSerializer
return super().get_serializer_class()
@action(["post"], detail=False)
def set_initial_password(self, request, *args, **kwargs):
return super().set_password(request, *args, **kwargs)
@action(["get", "patch"], detail=True, permission_classes=[CurrentUserOrAdmin])
def balance(self, request, *args, **kwargs):
user = self.get_object()
if request.method == "GET":
serializer = BonusProgramTransactionSerializer(user.bonus_history, many=True)
return Response(serializer.data)
elif request.method == "PATCH":
if not request.user.is_superuser:
return self.permission_denied(request)
# No balance underflow or dummy transactions allowed, no error will be raised
serializer = UserBalanceUpdateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.data
user.update_balance(amount=data['amount'], bonus_type=data['type'], comment=data['comment'])
list_serializer = BonusProgramTransactionSerializer(user.bonus_history, many=True)
return Response(list_serializer.data)
@action(["get"], url_path="me/balance", detail=False, permission_classes=[CurrentUserOrAdmin])
def me_balance(self, request, *args, **kwargs):
self.get_object = self.get_instance
return self.balance(request, *args, **kwargs)
class TelegramLoginForm(views.APIView):
permission_classes = [AllowAny]
def get_renderers(self):
if self.request.method == "GET" and settings.DEBUG:
return [StaticHTMLRenderer()]
return super().get_renderers()
def get(self, request, *args, **kwargs):
if not settings.DEBUG:
raise MethodNotAllowed(request.method)
source = """
<html>
<body>
<script async src="https://telegram.org/js/telegram-widget.js?22"
data-telegram-login="phzhik_dev_bot"
data-size="large"
data-onauth="onTelegramAuth(user)"
data-request-access="write"></script>
<script type="text/javascript">
function onTelegramAuth(user) {
console.log(user);
const request = new Request("/auth/telegram/", {
method: "post",
body: JSON.stringify(user),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
fetch(request)
.then(response => {
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
}
</script>
</body>
</html>
"""
return Response(source)
def post(self, request, *args, **kwargs):
serializer = TelegramCallbackSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except ValidationError as e:
return Response(e.detail, status=status.HTTP_401_UNAUTHORIZED)
except:
return Response(status=status.HTTP_401_UNAUTHORIZED)
data = serializer.data
# Authenticate user with given tg_user_id
tg_user_id = data["id"]
user: User = User.objects.filter(tg_user_id=tg_user_id).first()
if not user:
# Sign up user
user = User.objects.create_draft_user(tg_user_id=tg_user_id)
# Request the phone through the bot
request_phone_sync(tg_user_id, TGCoreMessage.SIGN_UP_SHARE_PHONE)
token = login_user(request, user)
return Response({"auth_token": token.key})

122
cdek/api.py Normal file
View File

@ -0,0 +1,122 @@
import http
import os
from time import sleep
from typing import Optional
from urllib.parse import urljoin
import requests
from django.conf import settings
from django.core.files.base import ContentFile
from requests import Request
from store.utils import is_migration_running
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
class CDEKClient:
AUTH_ENDPOINT = 'oauth/token'
ORDER_INFO_ENDPOINT = 'orders'
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
BARCODE_ENDPOINT = 'print/barcodes'
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)
retries = 0
while retries < self.MAX_RETRIES:
prepared = self.session.prepare_request(request)
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}'})
def get_order_info(self, im_number):
params = {
'im_number': str(im_number)
}
return self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
def create_order(self, order_data):
return self.request('POST', self.ORDER_INFO_ENDPOINT, json=order_data)
def edit_order(self, order_data):
return self.request('PATCH', self.ORDER_INFO_ENDPOINT, json=order_data)
def calculate_tariff(self, data):
return self.request('POST', self.CALCULATOR_TARIFF_ENDPOINT, json=data)
def generate_barcode(self, cdek_number, format="A6") -> Optional[str]:
request_data = {
"orders": [{"cdek_number": cdek_number}],
"copy_count": 1,
"format": format
}
r = self.request('POST', self.BARCODE_ENDPOINT, json=request_data)
if not r:
return None
resp_data = r.json()
if 'entity' not in resp_data:
return None
barcode_uuid = resp_data['entity']['uuid']
return barcode_uuid
def get_barcode_url(self, uuid) -> Optional[str]:
if not uuid:
return None
r = self.request('GET', f'{self.BARCODE_ENDPOINT}/{uuid}')
if not r:
return None
resp_data = r.json()
if 'entity' not in resp_data:
return None
url = resp_data['entity'].get('url')
return url
def get_barcode_file(self, cdek_number):
uuid = self.generate_barcode(cdek_number)
sleep(2) # Sometimes url are not yet created, so be prepared for this
url = self.get_barcode_url(uuid)
if not url:
return None
r = self.request('GET', url)
return ContentFile(r.content) if r and r.content else None
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
if not is_migration_running():
client.authorize()

View File

@ -1,211 +0,0 @@
import http
import os
from contextlib import suppress
from time import sleep
from typing import Optional
from urllib.parse import urljoin
import requests
from django.conf import settings
from django.core.files.base import ContentFile
from store.utils import is_migration_running
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
class CDEKStatus:
# Принят
ACCEPTED = "ACCEPTED"
# Создан
CREATED = "CREATED"
# Принят на склад отправителя
RECEIVED_AT_SHIPMENT_WAREHOUSE = "RECEIVED_AT_SHIPMENT_WAREHOUSE"
# Выдан на отправку в г. отправителе
READY_FOR_SHIPMENT_IN_SENDER_CITY = "READY_FOR_SHIPMENT_IN_SENDER_CITY"
# Возвращен на склад отправителя
RETURNED_TO_SENDER_CITY_WAREHOUSE = "RETURNED_TO_SENDER_CITY_WAREHOUSE"
# Сдан перевозчику в г. отправителе
TAKEN_BY_TRANSPORTER_FROM_SENDER_CITY = "TAKEN_BY_TRANSPORTER_FROM_SENDER_CITY"
# Отправлен в г. транзит
SENT_TO_TRANSIT_CITY = "SENT_TO_TRANSIT_CITY"
# Встречен в г. транзите
ACCEPTED_IN_TRANSIT_CITY = "ACCEPTED_IN_TRANSIT_CITY"
# Принят на склад транзита
ACCEPTED_AT_TRANSIT_WAREHOUSE = "ACCEPTED_AT_TRANSIT_WAREHOUSE"
# Возвращен на склад транзита
RETURNED_TO_TRANSIT_WAREHOUSE = "RETURNED_TO_TRANSIT_WAREHOUSE"
# Выдан на отправку в г. транзите
READY_FOR_SHIPMENT_IN_TRANSIT_CITY = "READY_FOR_SHIPMENT_IN_TRANSIT_CITY"
# Сдан перевозчику в г. транзите
TAKEN_BY_TRANSPORTER_FROM_TRANSIT_CITY = "TAKEN_BY_TRANSPORTER_FROM_TRANSIT_CITY"
# Отправлен в г. отправитель
SENT_TO_SENDER_CITY = "SENT_TO_SENDER_CITY"
# Отправлен в г. получатель
SENT_TO_RECIPIENT_CITY = "SENT_TO_RECIPIENT_CITY"
# Встречен в г. отправителе
ACCEPTED_IN_SENDER_CITY = "ACCEPTED_IN_SENDER_CITY"
# Встречен в г. получателе
ACCEPTED_IN_RECIPIENT_CITY = "ACCEPTED_IN_RECIPIENT_CITY"
# Принят на склад доставки
ACCEPTED_AT_RECIPIENT_CITY_WAREHOUSE = "ACCEPTED_AT_RECIPIENT_CITY_WAREHOUSE"
# Принят на склад до востребования
ACCEPTED_AT_PICK_UP_POINT = "ACCEPTED_AT_PICK_UP_POINT"
# Выдан на доставку
TAKEN_BY_COURIER = "TAKEN_BY_COURIER"
# Возвращен на склад доставки
RETURNED_TO_RECIPIENT_CITY_WAREHOUSE = "RETURNED_TO_RECIPIENT_CITY_WAREHOUSE"
# Вручен
DELIVERED = "DELIVERED"
# Не вручен
NOT_DELIVERED = "NOT_DELIVERED"
# Некорректный заказ
INVALID = "INVALID"
# Таможенное оформление в стране отправления
IN_CUSTOMS_INTERNATIONAL = "IN_CUSTOMS_INTERNATIONAL"
# Отправлено в страну назначения
SHIPPED_TO_DESTINATION = "SHIPPED_TO_DESTINATION"
# Передано транзитному перевозчику
PASSED_TO_TRANSIT_CARRIER = "PASSED_TO_TRANSIT_CARRIER"
# Таможенное оформление в стране назначения
IN_CUSTOMS_LOCAL = "IN_CUSTOMS_LOCAL"
# Таможенное оформление завершено
CUSTOMS_COMPLETE = "CUSTOMS_COMPLETE"
# Заложен в постамат
POSTOMAT_POSTED = "POSTOMAT_POSTED"
# Изъят из постамата курьером
POSTOMAT_SEIZED = "POSTOMAT_SEIZED"
# Изъят из постамата клиентом
POSTOMAT_RECEIVED = "POSTOMAT_RECEIVED"
class CDEKClient:
AUTH_ENDPOINT = 'oauth/token'
ORDER_INFO_ENDPOINT = 'orders'
CALCULATOR_TARIFF_ENDPOINT = 'calculator/tariff'
CALCULATOR_TARIFF_LIST_ENDPOINT = 'calculator/tarifflist'
BARCODE_ENDPOINT = 'print/barcodes'
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 = requests.Request(method, joined_url, *args, **kwargs)
retries = 0
while retries < self.MAX_RETRIES:
retries += 1
prepared = self.session.prepare_request(request)
try:
r = self.session.send(prepared, timeout=settings.EXTERNAL_API_TIMEOUT_SEC)
except:
continue
# 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}'})
def get_order_info(self, im_number):
params = {
'im_number': str(im_number)
}
return self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
def create_order(self, order_data):
return self.request('POST', self.ORDER_INFO_ENDPOINT, json=order_data)
def edit_order(self, order_data):
return self.request('PATCH', self.ORDER_INFO_ENDPOINT, json=order_data)
def calculate_tariff(self, data):
return self.request('POST', self.CALCULATOR_TARIFF_ENDPOINT, json=data)
def calculate_tarifflist(self, data):
return self.request('POST', self.CALCULATOR_TARIFF_LIST_ENDPOINT, json=data)
def generate_barcode(self, cdek_number, format="A6") -> Optional[str]:
request_data = {
"orders": [{"cdek_number": cdek_number}],
"copy_count": 1,
"format": format
}
r = self.request('POST', self.BARCODE_ENDPOINT, json=request_data)
if not r:
return None
resp_data = r.json()
if 'entity' not in resp_data:
return None
barcode_uuid = resp_data['entity']['uuid']
return barcode_uuid
def get_barcode_url(self, uuid) -> Optional[str]:
if not uuid:
return None
r = self.request('GET', f'{self.BARCODE_ENDPOINT}/{uuid}')
if not r:
return None
resp_data = r.json()
if 'entity' not in resp_data:
return None
url = resp_data['entity'].get('url')
return url
def get_barcode_file(self, cdek_number):
uuid = self.generate_barcode(cdek_number)
sleep(2) # Sometimes url are not yet created, so be prepared for this
url = self.get_barcode_url(uuid)
if not url:
return None
r = self.request('GET', url)
return ContentFile(r.content) if r and r.content else None
def get_order_statuses(self, cdek_number):
params = {
'cdek_number': str(cdek_number)
}
r = self.request('GET', self.ORDER_INFO_ENDPOINT, params=params)
if not r:
return []
with suppress(KeyError):
statuses = r.json()['entity']['statuses']
statuses = [s.get('code') for s in statuses]
return statuses
return []
client = CDEKClient(settings.CDEK_CLIENT_ID, settings.CDEK_CLIENT_SECRET)
if not is_migration_running():
client.authorize()

View File

@ -1,57 +0,0 @@
from decimal import Decimal
from contextlib import suppress
from urllib.parse import urljoin
import requests
from django.conf import settings
class CurrencyAPIClient:
CONVERT_ENDPOINT = 'currency/convert'
MAX_RETRIES = 2
def __init__(self, api_key: str):
self.api_url = 'https://api.getgeoapi.com/v2/'
self.api_key = api_key
self.session = requests.Session()
def request(self, method, url, *args, **kwargs):
params = kwargs.pop('params', {})
params.update({"api_key": self.api_key})
joined_url = urljoin(self.api_url, url)
request = requests.Request(method, joined_url, params=params, *args, **kwargs)
retries = 0
while retries < self.MAX_RETRIES:
retries += 1
prepared = self.session.prepare_request(request)
r = self.session.send(prepared, timeout=settings.EXTERNAL_API_TIMEOUT_SEC)
# TODO: handle/log errors
return r
def get_rate(self, currency1: str, currency2: str, amount=1):
params = {
'from': currency1,
'to': currency2,
'amount': amount,
'format': 'json'
}
r = self.request('GET', self.CONVERT_ENDPOINT, params=params)
if not r or r.json().get('status') == 'failed':
return None
with suppress(KeyError):
rate = r.json()['rates'][currency2.upper()]['rate']
return Decimal(rate)
return None
def get_cny_rate(self):
return self.get_rate('cny', 'rub')
client = CurrencyAPIClient(settings.CURRENCY_GETGEOIP_API_KEY)

View File

@ -1,52 +0,0 @@
from urllib3.util import parse_url
from urllib.parse import urljoin, parse_qs
from django.conf import settings
import requests
class PoizonClient:
SPU_GET_DATA_ENDPOINT = 'spu_get_data'
MAX_RETRIES = 2
def __init__(self, token: str):
self.api_url = 'http://124.222.99.75/'
self.token = token
self.session = requests.Session()
def request(self, method, url, *args, **kwargs):
params = kwargs.pop('params', {})
params.update({"token": self.token})
joined_url = urljoin(self.api_url, url)
request = requests.Request(method, joined_url, params=params, *args, **kwargs)
retries = 0
while retries < self.MAX_RETRIES:
retries += 1
prepared = self.session.prepare_request(request)
r = self.session.send(prepared, timeout=settings.EXTERNAL_API_TIMEOUT_SEC)
# TODO: handle/log errors
return r
@staticmethod
def get_spu_id(url):
try:
# Go to short dw4.co url to get the full one from redirect
if 'dw4.co' in url:
r = requests.get(url)
url = r.url
url = parse_url(url)
qs = parse_qs(url.query)
spu_id = qs.get('spuId')
return spu_id.pop(0) if spu_id else None
except Exception as exc:
# TODO: handle/log errors
return None
def get_good_info(self, spu_id):
params = {'spuId': str(spu_id)}
return self.request('GET', self.SPU_GET_DATA_ENDPOINT, params=params)

View File

@ -1,3 +0,0 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

View File

@ -1,27 +0,0 @@
import os
from datetime import timedelta
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
app = Celery('poizonstore')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
app.conf.beat_schedule = {
'update-cdek-status-every-hour': {
'task': 'store.tasks.schedule_cdek_status_update',
'schedule': timedelta(hours=1),
},
'update-yuan-rate-every-hour': {
'task': 'store.tasks.update_yuan_rate',
'schedule': timedelta(hours=1),
},
}
@app.task()
def debug_task():
print(f'Task complete')

View File

@ -12,48 +12,25 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
import os
from pathlib import Path
import sentry_sdk
from django.core.exceptions import ImproperlyConfigured
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
def get_secret(setting):
"""Get the secret variable or return explicit exception."""
try:
return os.environ[setting]
except KeyError:
error_msg = f'Set the {setting} environment variable'
raise ImproperlyConfigured(error_msg)
# 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 = get_secret("SECRET_KEY")
SECRET_KEY = 'django-insecure-e&9j(^9z7p7qs-@d)vftjz4%xqu0#3mmn@+$wzwh!%-dwjecm-'
# External API settings
CDEK_CLIENT_ID = get_secret("CDEK_CLIENT_ID")
CDEK_CLIENT_SECRET = get_secret("CDEK_CLIENT_SECRET")
POIZON_TOKEN = get_secret("POIZON_TOKEN")
CURRENCY_GETGEOIP_API_KEY = get_secret("CURRENCY_GETGEOIP_API_KEY")
EXTERNAL_API_TIMEOUT_SEC = 60
# Telegram bot
TG_BOT_TOKEN = get_secret("TG_BOT_TOKEN")
CDEK_CLIENT_ID = 'wZWtjnWtkX7Fin2tvDdUE6eqYz1t1GND'
CDEK_CLIENT_SECRET = 'lc2gmrmK5s1Kk6FhZbNqpQCaATQRlsOy'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG") or 0))
DISABLE_PERMISSIONS = False
DISABLE_CORS = True
ALLOWED_HOSTS = get_secret('ALLOWED_HOSTS').split(',')
ALLOWED_HOSTS = ["crm-poizonstore.ru", "127.0.0.1", "localhost", "45.84.227.72"]
INTERNAL_IPS = ["127.0.0.1", 'localhost']
@ -71,12 +48,7 @@ CORS_ALLOWED_ORIGINS = [
if DISABLE_CORS:
CORS_ALLOW_ALL_ORIGINS = True
# Required for "Login via Telegram" popup
# Source: https://stackoverflow.com/a/73240366/24046062
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin-allow-popups'
AUTH_USER_MODEL = 'account.User'
PHONENUMBER_DEFAULT_REGION = 'RU'
AUTH_USER_MODEL = 'store.User'
# Application definition
@ -98,9 +70,7 @@ INSTALLED_APPS = [
'django_filters',
'mptt',
'account',
'store',
'tg_bot'
'store'
]
MIDDLEWARE = [
@ -185,13 +155,12 @@ REST_FRAMEWORK = {
}
DJOSER = {
'LOGIN_FIELD': 'email',
'TOKEN_MODEL': 'rest_framework.authtoken.models.Token',
'SERIALIZERS': {
'user': 'account.serializers.UserSerializer',
'current_user': 'account.serializers.UserSerializer',
'user_create': 'account.serializers.UserCreateSerializer',
'token_create': 'account.serializers.TokenCreateSerializer',
'user': 'store.serializers.UserSerializer',
'current_user': 'store.serializers.UserSerializer',
},
}
@ -224,38 +193,4 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CHECKLIST_ID_LENGTH = 10
REFERRAL_CODE_LENGTH = 10
COMMISSION_OVER_150K = 1.1
# Logging
SENTRY_DSN = "https://96106e3f938badc86ecb2e502716e496@o4506163299418112.ingest.sentry.io/4506163300663296"
if not DEBUG:
sentry_sdk.init(
dsn=SENTRY_DSN,
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
traces_sample_rate=1.0,
# Set profiles_sample_rate to 1.0 to profile 100%
# of sampled transactions.
# We recommend adjusting this value in production.
profiles_sample_rate=1.0,
)
# Celery
BROKER_URL = 'redis://localhost:6379/2'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
# Bonus program
# TODO: move to GlobalSettings?
BONUS_PROGRAM_CONFIG = {
"amounts": {
"signup": 150,
"default_purchase": 50,
"referral": 500,
}
}

View File

@ -24,7 +24,8 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('__debug__/', include('debug_toolbar.urls')),
path('', include('store.urls')),
path('', include('account.urls')),
path('', include('djoser.urls')),
path('auth/', include('djoser.urls.authtoken')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL)

View File

@ -8,27 +8,11 @@ django-cors-headers==4.1.0
djoser==2.2.0
drf-extra-fields==3.5.0
Pillow==9.5.0
django-phonenumber-field[phonenumberslite]
# Tasks
celery==5.3.6
redis==5.0.1
flower==2.0.1
# Telegram bot
pyTelegramBotAPI==4.17.0
aiohttp==3.9.4
# Misc
tqdm==4.65.0
django-debug-toolbar==4.1.0
requests==2.31.0
# Logging
sentry-sdk==1.34.0
sentry-telegram-py3==0.6.1
# Deployment
# gunicorn==20.1.0
uWSGI==2.0.21
inotify==0.2.10

View File

@ -1,37 +0,0 @@
import os
import asyncio
import logging
import sys
import django
import telebot
from telebot import asyncio_filters
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poizonstore.settings')
django.setup()
from tg_bot.bot import bot, commands
from tg_bot.handlers import register_handlers
logger = telebot.logger
telebot.logger.setLevel(logging.DEBUG)
async def setup():
await bot.delete_my_commands(scope=None, language_code=None)
bot.add_custom_filter(asyncio_filters.StateFilter(bot))
register_handlers()
await bot.set_my_commands(commands=commands)
async def main():
logger.info("bot starting...")
await setup()
await bot.infinity_polling()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -2,7 +2,12 @@ from django.contrib import admin
from django.contrib.admin import display
from mptt.admin import MPTTModelAdmin
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, Image, Gift
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)
@ -47,10 +52,5 @@ class PromoCodeAdmin(admin.ModelAdmin):
list_display = ('name', 'discount', 'free_delivery', 'no_comission')
@admin.register(Gift)
class GiftAdmin(admin.ModelAdmin):
list_display = ('name', 'min_price')

View File

@ -10,3 +10,12 @@ class CRMException(APIException):
detail = self.default_detail
self.detail = {'error': detail}
class UnauthorizedException(CRMException):
"""Authentication exception error mixin."""
status_code = status.HTTP_401_UNAUTHORIZED
class InvalidCredentialsException(UnauthorizedException):
default_detail = 'cannot find the worker'

View File

@ -1,22 +0,0 @@
from django_filters import rest_framework as filters
from .models import Checklist, Gift
class GiftFilter(filters.FilterSet):
for_price = filters.NumberFilter(method='filter_for_price')
class Meta:
model = Gift
fields = ('for_price',)
def filter_for_price(self, queryset, name, value):
return queryset.filter(min_price__lte=value)
class ChecklistFilter(filters.FilterSet):
status = filters.MultipleChoiceFilter(choices=Checklist.Status.CHOICES)
class Meta:
model = Checklist
fields = ('status',)

View File

@ -2,33 +2,24 @@ from django.contrib.auth.hashers import make_password
from django.core.management import BaseCommand
from tqdm import tqdm
from account.models import User
from store.models import User
users = [
{
"email": "poizonstore@mail.ru",
"password": "219404Poizon",
"first_name": "Илья",
"middle_name": "Сергеевич",
"last_name": "Савочкин",
"role": User.ADMIN,
"job_title": User.ADMIN,
"is_staff": True
},
{
"email": "poizonmanager1@mail.ru",
"password": "poizonm1",
"first_name": "Патрик",
"middle_name": "Сергеевич",
"last_name": "Стар",
"role": User.PRODUCT_MANAGER
"job_title": User.PRODUCT_MANAGER
},
{
"email": "poizonorder1@mail.ru",
"password": "2193071Po1",
"first_name": "Гоша",
"middle_name": "Альбах",
"last_name": "Абызов",
"role": User.ORDER_MANAGER
"job_title": User.ORDER_MANAGER
}
]

View File

@ -1,11 +1,9 @@
# Generated by Django 4.2.2 on 2024-03-28 22:05
# Generated by Django 4.2.2 on 2023-06-30 22:04
import datetime
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
import django.utils.timezone
import store.models
@ -14,149 +12,96 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('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='Название')),
('delivery_price_CN_RU', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки Китай-РФ')),
('commission', models.DecimalField(decimal_places=2, default=0, max_digits=10, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Дополнительная комиссия, %')),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='store.category', verbose_name='Родительская категория')),
('slug', models.SlugField(verbose_name='Идентификатор')),
],
options={
'verbose_name': 'Категория',
'verbose_name_plural': 'Категории',
},
),
migrations.CreateModel(
name='Gift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название')),
('image', models.ImageField(blank=True, null=True, upload_to='gifts/', verbose_name='Фото')),
('min_price', models.DecimalField(decimal_places=2, default=0, help_text='от какой суммы доступен подарок', max_digits=10, verbose_name='Минимальная цена в юанях')),
('available_count', models.PositiveSmallIntegerField(default=0, verbose_name='Доступное количество')),
],
options={
'verbose_name': 'Подарок',
'verbose_name_plural': 'Подарки',
},
),
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, default=0, max_digits=10, verbose_name='Курс CNY/RUB')),
('yuan_rate_last_updated', models.DateTimeField(default=None, null=True, verbose_name='Дата обновления курса CNY/RUB')),
('yuan_rate_commission', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Наценка на курс юаня, руб')),
('delivery_price_CN', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки по Китаю')),
('commission_rub', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Комиссия, руб')),
('pickup_address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Адрес пункта самовывоза')),
('time_to_buy', models.DurationField(default=datetime.timedelta(seconds=10800), help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'", verbose_name='Время на покупку')),
],
options={
'verbose_name': 'Глобальные настройки',
'verbose_name_plural': 'Глобальные настройки',
},
),
migrations.CreateModel(
name='Image',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to=store.models.image_upload_path, verbose_name='Файл изображения')),
('type', models.PositiveSmallIntegerField(choices=[(0, 'Изображение'), (1, 'Превью'), (2, 'Документ'), (3, 'Подарок')], default=0, verbose_name='Тип')),
],
options={
'verbose_name': 'Изображение',
'verbose_name_plural': 'Изображения',
},
),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, verbose_name='Название')),
('slug', models.SlugField(unique=True, verbose_name='Идентификатор')),
('cardnumber', models.CharField(blank=True, max_length=30, null=True, verbose_name='Номер карты')),
('requisites', models.CharField(blank=True, max_length=200, null=True, verbose_name='Реквизиты')),
],
options={
'verbose_name': 'Метод оплаты',
'verbose_name_plural': 'Методы оплаты',
},
),
migrations.CreateModel(
name='PriceSnapshot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yuan_rate', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Курс CNY/RUB')),
('delivery_price_CN', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки по Китаю')),
('delivery_price_CN_RU', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки Китай-РФ')),
('commission_rub', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Комиссия, руб')),
('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',
name='PromoCode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Название')),
('discount', models.PositiveIntegerField(verbose_name='Скидка в рублях')),
('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='Без комиссии')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
],
options={
'verbose_name': 'Промокод',
'verbose_name_plural': 'Промокоды',
},
),
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(auto_now_add=True, verbose_name='Дата обновления статуса заказа')),
('id', models.CharField(default=store.models.generate_checklist_id, editable=False, max_length=10, primary_key=True, serialize=False)),
('status', models.CharField(choices=[('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('bought', 'Закуплен'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('split_waiting', 'Сплит: ожидание оплаты 2й части'), ('split_paid', 'Сплит: полностью оплачено'), ('cdek', 'Доставляется СДЭК'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа')),
('product_link', models.URLField(blank=True, null=True, verbose_name='Ссылка на товар')),
('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.CharField(blank=True, max_length=30, null=True, verbose_name='Размер')),
('price_yuan', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена в юанях')),
('real_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, 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='Телефон получателя')),
('is_split_payment', models.BooleanField(default=False, verbose_name='Оплата частями')),
('payment_proof', models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Подтверждение оплаты')),
('split_payment_proof', models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Подтверждение оплаты сплита')),
('split_accepted', models.BooleanField(default=False, verbose_name='Сплит принят')),
('receipt', models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Фото чека')),
('delivery', models.CharField(blank=True, choices=[('pickup', 'Самовывоз из шоурума'), ('cdek', 'Пункт выдачи заказов CDEK'), ('cdek_courier', 'Курьерская доставка CDEK')], max_length=15, null=True, verbose_name='Тип доставки')),
('poizon_tracking', models.CharField(blank=True, max_length=100, null=True, verbose_name='Трек-номер Poizon')),
('cdek_tracking', models.CharField(blank=True, max_length=100, null=True, verbose_name='Трек-номер СДЭК')),
('cdek_barcode_pdf', models.FileField(blank=True, null=True, upload_to='docs', verbose_name='Штрих-код СДЭК в PDF')),
('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='Категория')),
('gift', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.gift', verbose_name='Подарок')),
('images', models.ManyToManyField(blank=True, related_name='+', to='store.image', verbose_name='Изображения')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Менеджер')),
('payment_method', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.paymentmethod', verbose_name='Метод оплаты')),
('price_snapshot', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checklist', to='store.pricesnapshot', verbose_name='Сохраненные цены')),
('promocode', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='store.promocode', verbose_name='Промокод')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Заказ',
'verbose_name_plural': 'Заказы',
},
),
]

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

@ -1,26 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-07 23:27
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('store', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='checklist',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_orders', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='checklist',
name='manager',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='manager_orders', to=settings.AUTH_USER_MODEL, 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

@ -1,25 +0,0 @@
# Generated by Django 4.2.2 on 2024-04-21 03:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0002_checklist_customer_alter_checklist_manager'),
]
operations = [
migrations.RemoveField(
model_name='checklist',
name='buyer_name',
),
migrations.RemoveField(
model_name='checklist',
name='buyer_phone',
),
migrations.RemoveField(
model_name='checklist',
name='buyer_telegram',
),
]

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

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 20:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0008_alter_checklist_id'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='status',
field=models.CharField(choices=[('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 21:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0009_alter_checklist_status'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='status',
field=models.CharField(choices=[('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('bought', 'Закуплен'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('cdek', 'Доставляется СДЭК'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.2 on 2023-07-01 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0010_alter_checklist_status'),
]
operations = [
migrations.RemoveField(
model_name='globalsettings',
name='delivery_price_CN_RU',
),
migrations.AddField(
model_name='category',
name='delivery_price_CN_RU',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.2 on 2023-07-01 21:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0011_remove_globalsettings_delivery_price_cn_ru_and_more'),
]
operations = [
migrations.AlterField(
model_name='category',
name='delivery_price_CN_RU',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки Китай-РФ'),
),
migrations.AlterField(
model_name='globalsettings',
name='commission_rub',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Комиссия, руб'),
),
migrations.AlterField(
model_name='globalsettings',
name='delivery_price_CN',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки по Китаю'),
),
migrations.AlterField(
model_name='globalsettings',
name='yuan_rate',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Курс CNY/RUB'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 4.2.2 on 2023-07-01 22:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('store', '0012_alter_category_delivery_price_cn_ru_and_more'),
]
operations = [
migrations.CreateModel(
name='PaymentType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('requisites', models.CharField(max_length=200, verbose_name='Реквизиты')),
],
options={
'verbose_name': 'Метод оплаты',
'verbose_name_plural': 'Методы оплаты',
},
),
migrations.AlterModelOptions(
name='category',
options={'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'},
),
migrations.AlterModelOptions(
name='globalsettings',
options={'verbose_name': 'Глобальные настройки', 'verbose_name_plural': 'Глобальные настройки'},
),
migrations.AlterField(
model_name='checklist',
name='payment_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='store.paymenttype', verbose_name='Метод оплаты'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.2 on 2023-07-01 22:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0013_paymenttype_alter_category_options_and_more'),
]
operations = [
migrations.RenameModel(
old_name='PaymentType',
new_name='PaymentMethod',
),
migrations.RenameField(
model_name='checklist',
old_name='payment_type',
new_name='payment_method',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-01 22:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0014_rename_paymenttype_paymentmethod_and_more'),
]
operations = [
migrations.AddField(
model_name='paymentmethod',
name='name',
field=models.CharField(default='', max_length=30, verbose_name='Название'),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-01 22:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0015_paymentmethod_name'),
]
operations = [
migrations.AddField(
model_name='paymentmethod',
name='cardnumber',
field=models.CharField(default='', max_length=30, verbose_name='Номер карты'),
preserve_default=False,
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.2 on 2023-07-01 23:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0016_paymentmethod_cardnumber'),
]
operations = [
migrations.AlterModelOptions(
name='promocode',
options={'verbose_name': 'Промокод', 'verbose_name_plural': 'Промокоды'},
),
migrations.AlterField(
model_name='paymentmethod',
name='slug',
field=models.SlugField(unique=True, verbose_name='Идентификатор'),
),
migrations.AlterField(
model_name='promocode',
name='discount',
field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Скидка'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-01 23:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0017_alter_promocode_options_alter_paymentmethod_slug_and_more'),
]
operations = [
migrations.AlterField(
model_name='promocode',
name='name',
field=models.CharField(max_length=100, unique=True, verbose_name='Название'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.2 on 2023-07-01 23:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0018_alter_promocode_name'),
]
operations = [
migrations.AddField(
model_name='globalsettings',
name='pickup_address',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Адрес пункта самовывоза'),
),
migrations.AlterField(
model_name='checklist',
name='size',
field=models.CharField(blank=True, max_length=30, null=True, verbose_name='Размер'),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.2 on 2023-07-02 15:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0019_globalsettings_pickup_address_alter_checklist_size'),
]
operations = [
migrations.CreateModel(
name='Image',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='checklist_images')),
('is_preview', models.BooleanField(default=False)),
],
),
migrations.RemoveField(
model_name='checklist',
name='image',
),
migrations.RemoveField(
model_name='checklist',
name='preview_image',
),
migrations.AddField(
model_name='checklist',
name='images',
field=models.ManyToManyField(blank=True, to='store.image', verbose_name='Картинки'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-02 17:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0020_image_remove_checklist_image_and_more'),
]
operations = [
migrations.RenameField(
model_name='image',
old_name='is_preview',
new_name='needs_preview',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-02 17:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0021_rename_is_preview_image_needs_preview'),
]
operations = [
migrations.RenameField(
model_name='image',
old_name='needs_preview',
new_name='is_preview',
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.2 on 2023-07-02 23:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0022_rename_needs_preview_image_is_preview'),
]
operations = [
migrations.AlterModelOptions(
name='checklist',
options={'verbose_name': 'Заказ', 'verbose_name_plural': 'Заказы'},
),
migrations.AlterModelOptions(
name='image',
options={'verbose_name': 'Изображение', 'verbose_name_plural': 'Изображения'},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.2 on 2023-07-03 10:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('store', '0023_alter_checklist_options_alter_image_options'),
]
operations = [
migrations.RemoveField(
model_name='checklist',
name='images',
),
migrations.AddField(
model_name='image',
name='checklist',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='store.checklist'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-03 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0024_remove_checklist_images_image_checklist'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='status_updated_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата обновления статуса заказа'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.2 on 2023-07-04 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0025_alter_checklist_status_updated_at'),
]
operations = [
migrations.RemoveField(
model_name='checklist',
name='track_number',
),
migrations.AddField(
model_name='checklist',
name='cdek_tracking',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Трек-номер СДЭК'),
),
migrations.AddField(
model_name='checklist',
name='poizon_tracking',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Трек-номер Poizon'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-07-05 01:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0026_remove_checklist_track_number_and_more'),
]
operations = [
migrations.AddField(
model_name='checklist',
name='cdek_barcode_pdf',
field=models.ImageField(blank=True, null=True, upload_to='docs', verbose_name='Штрих-код СДЭК в PDF'),
),
migrations.AlterField(
model_name='checklist',
name='cheque_photo',
field=models.ImageField(blank=True, null=True, upload_to='docs', verbose_name='Фото чека'),
),
migrations.AlterField(
model_name='checklist',
name='payment_proof',
field=models.ImageField(blank=True, null=True, upload_to='docs', verbose_name='Подтверждение оплаты'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-05 02:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0027_checklist_cdek_barcode_pdf_and_more'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='cdek_barcode_pdf',
field=models.FileField(blank=True, null=True, upload_to='docs', verbose_name='Штрих-код СДЭК в PDF'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.2 on 2023-07-06 11:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0028_alter_checklist_cdek_barcode_pdf'),
]
operations = [
migrations.RenameField(
model_name='checklist',
old_name='cheque_photo',
new_name='receipt',
),
migrations.AlterField(
model_name='checklist',
name='status',
field=models.CharField(choices=[('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('bought', 'Закуплен'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('split_waiting', 'Сплит: ожидание оплаты 2й части'), ('split_paid', 'Сплит: полностью оплачено'), ('cdek', 'Доставляется СДЭК'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа'),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.2 on 2024-04-05 21:35
# Generated by Django 4.2.2 on 2023-07-06 12:00
from django.db import migrations, models
@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0006_user_tg_user_id'),
('store', '0029_rename_cheque_photo_checklist_receipt_and_more'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_draft_user',
field=models.BooleanField(default=False, verbose_name='Черновик пользователя'),
model_name='checklist',
name='is_split_payment',
field=models.BooleanField(default=False, verbose_name='Оплата частями'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-06 12:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('store', '0030_checklist_is_split_payment'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='promocode',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='store.promocode', verbose_name='Промокод'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-06 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0031_alter_checklist_promocode'),
]
operations = [
migrations.AddField(
model_name='promocode',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Активен'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.2 on 2023-07-06 13:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('store', '0032_promocode_is_active'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='manager_id',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-06 21:32
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0033_remove_user_manager_id'),
]
operations = [
migrations.AlterField(
model_name='promocode',
name='discount',
field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Скидка'),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2.2 on 2023-07-07 03:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0034_alter_promocode_discount'),
]
operations = [
migrations.RemoveField(
model_name='image',
name='checklist',
),
migrations.AddField(
model_name='checklist',
name='images',
field=models.ManyToManyField(related_name='+', to='store.image', verbose_name='Изображения'),
),
migrations.RemoveField(
model_name='checklist',
name='payment_proof',
),
migrations.AddField(
model_name='checklist',
name='payment_proof',
field=models.ManyToManyField(related_name='+', to='store.image', verbose_name='Подтверждение оплаты'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.2 on 2023-07-07 03:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0035_remove_image_checklist_checklist_images_and_more'),
]
operations = [
migrations.RemoveField(
model_name='image',
name='is_preview',
),
migrations.AddField(
model_name='image',
name='type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Изображение'), (1, 'Превью')], default=0, verbose_name='Тип'),
),
migrations.AlterField(
model_name='checklist',
name='receipt',
field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото чека'),
),
migrations.AlterField(
model_name='image',
name='image',
field=models.ImageField(upload_to='', verbose_name='Файл изображения'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-07-07 03:23
from django.db import migrations, models
import store.models
class Migration(migrations.Migration):
dependencies = [
('store', '0036_remove_image_is_preview_image_type_and_more'),
]
operations = [
migrations.AlterField(
model_name='image',
name='image',
field=models.ImageField(upload_to=store.models.image_upload_path, verbose_name='Файл изображения'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.2 on 2023-07-07 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0037_alter_image_image'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='images',
field=models.ManyToManyField(blank=True, related_name='+', to='store.image', verbose_name='Изображения'),
),
migrations.AlterField(
model_name='checklist',
name='payment_proof',
field=models.ManyToManyField(blank=True, related_name='+', to='store.image', verbose_name='Подтверждение оплаты'),
),
migrations.AlterField(
model_name='checklist',
name='receipt',
field=models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Фото чека'),
),
migrations.AlterField(
model_name='image',
name='type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Изображение'), (1, 'Превью'), (2, 'Документ')], default=0, verbose_name='Тип'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-10 17:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0038_alter_checklist_images_alter_checklist_payment_proof_and_more'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='delivery',
field=models.CharField(blank=True, choices=[('pickup', 'Самовывоз из шоурума'), ('cdek', 'Пункт выдачи заказов CDEK'), ('cdek_courier', 'Курьерская доставка CDEK')], max_length=15, null=True, verbose_name='Тип доставки'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.2 on 2023-07-12 20:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0039_alter_checklist_delivery'),
]
operations = [
migrations.AlterField(
model_name='paymentmethod',
name='cardnumber',
field=models.CharField(blank=True, max_length=30, null=True, verbose_name='Номер карты'),
),
migrations.AlterField(
model_name='paymentmethod',
name='requisites',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Реквизиты'),
),
]

View File

@ -0,0 +1,134 @@
# Generated by Django 4.2.2 on 2023-08-19 16:45
import datetime
import django.core.validators
from django.conf import settings
from django.db import migrations, models, transaction
import django.db.models.deletion
import mptt.fields
from mptt import register, managers
from store.management.commands.create_initial_data import create_categories
def create_initial_categories(apps, schema_editor):
create_categories()
# Dummy model with the removed field(s)
class OldChecklist(models.Model):
id = models.CharField(primary_key=True, max_length=settings.CHECKLIST_ID_LENGTH)
category_id = models.PositiveIntegerField()
subcategory = models.CharField('Подкатегория', max_length=20, blank=True, null=True)
class Meta:
managed = False
db_table = 'store_checklist'
# Add mptt fields to Category
# Loop through orders, save subcategory names and category ids
# Create subcategories, link to parent category
# Loop through orders, if subcategory was present, set it as category
# Perform migration - remove subcategory field
def create_subcategories(apps, schema_editor):
Checklist = apps.get_model("store", "Checklist")
Category = apps.get_model("store", "Category")
# Loop through orders, save subcategory names and category ids
# Create subcategories, link to parent category
# If Checklist had subcategory, set it as category
with transaction.atomic():
for checklist in OldChecklist.objects.all():
if checklist.subcategory is None or checklist.category_id is None:
continue
category_data = {'name': checklist.subcategory, 'parent_id': checklist.category_id}
# just to overcome not-null constraint errors for mptt
mptt_data = {'level': 0, 'lft': 0, 'rght': 0, 'tree_id': 0}
subcat_obj, _ = Category.objects.get_or_create(**category_data, defaults=mptt_data)
# To really update the Checklist, we must use a real model instead of the dummy OldChecklist one
Checklist.objects.filter(id=checklist.id).update(category_id=subcat_obj.id)
def rebuild_tree(apps, schema_editor):
model = apps.get_model('store', 'Category')
manager = managers.TreeManager()
manager.model = model
register(model)
manager.contribute_to_class(model, 'objects')
manager.rebuild()
class Migration(migrations.Migration):
dependencies = [
('store', '0040_alter_paymentmethod_cardnumber_and_more'),
]
operations = [
migrations.RemoveField(
model_name='category',
name='slug',
),
migrations.AddField(
model_name='category',
name='commission',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Дополнительная комиссия, %'),
),
migrations.AddField(
model_name='category',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='category',
name='lft',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='category',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='store.category', verbose_name='Родительская категория'),
),
migrations.AddField(
model_name='category',
name='rght',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='category',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
migrations.RunPython(code=create_initial_categories),
migrations.RunPython(code=create_subcategories),
migrations.RunPython(code=rebuild_tree),
migrations.AddField(
model_name='globalsettings',
name='time_to_buy',
field=models.DurationField(default=datetime.timedelta(seconds=10800), help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'", verbose_name='Время на покупку'),
),
migrations.AlterField(
model_name='promocode',
name='discount',
field=models.PositiveIntegerField(verbose_name='Скидка в рублях'),
),
migrations.RemoveField(
model_name='checklist',
name='subcategory',
),
]

View File

@ -0,0 +1,59 @@
# Generated by Django 4.2.2 on 2023-08-21 10:41
from django.db import migrations, models
import django.db.models.deletion
def move_m2m_payment_proof_to_image(apps, schema_editor):
Checklist = apps.get_model('store', 'Checklist')
for checklist in Checklist.objects.all():
img_obj = checklist.payment_proof.all().first()
if img_obj:
checklist._payment_proof = img_obj.image
checklist.save()
class Migration(migrations.Migration):
dependencies = [
('store', '0041_remove_category_slug_remove_checklist_subcategory_and_more'),
]
operations = [
migrations.CreateModel(
name='OldChecklist',
fields=[
('id', models.CharField(max_length=10, primary_key=True, serialize=False)),
('category_id', models.PositiveIntegerField()),
('subcategory', models.CharField(blank=True, max_length=20, null=True, verbose_name='Подкатегория')),
],
options={
'db_table': 'store_checklist',
'managed': False,
},
),
migrations.AddField(
model_name='checklist',
name='_payment_proof',
field=models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Подтверждение оплаты'),
),
migrations.RunPython(code=move_m2m_payment_proof_to_image),
migrations.RemoveField(
model_name='checklist',
name='payment_proof',
),
migrations.RenameField(
model_name='checklist',
old_name='_payment_proof',
new_name='payment_proof',
),
migrations.AddField(
model_name='checklist',
name='split_payment_proof',
field=models.ImageField(blank=True, null=True, upload_to='docs/', verbose_name='Подтверждение оплаты сплита'),
),
]

View File

@ -1,32 +1,34 @@
import math
import posixpath
import random
import string
from datetime import timedelta
from decimal import Decimal
import random
import string
from io import BytesIO
from typing import Optional
from django.conf import settings
from django.contrib.admin import display
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser, UserManager as _UserManager
from django.core.files.base import ContentFile
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q
from django.db.models.functions import Ceil
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 mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
from store.utils import create_preview
from store.utils import create_preview, concat_not_null_values
from utils.cache import InMemoryCache
class GlobalSettings(models.Model):
# currency
yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
yuan_rate_last_updated = models.DateTimeField('Дата обновления курса CNY/RUB', null=True, default=None)
yuan_rate_commission = models.DecimalField('Наценка на курс юаня, руб', 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)
@ -45,17 +47,20 @@ class GlobalSettings(models.Model):
self.__class__.objects.exclude(id=self.id).delete()
super().save(*args, **kwargs)
InMemoryCache.set('GlobalSettings', self)
def __str__(self) -> str:
return f'GlobalSettings <{self.id}>'
@classmethod
def load(cls) -> 'GlobalSettings':
obj, _ = cls.objects.get_or_create(id=1)
return obj
cached = InMemoryCache.get('GlobalSettings')
if cached:
return cached
@property
def full_yuan_rate(self):
return self.yuan_rate + self.yuan_rate_commission
obj, _ = cls.objects.get_or_create(id=1)
InMemoryCache.set('GlobalSettings', obj)
return obj
class Category(MPTTModel):
@ -77,20 +82,66 @@ class Category(MPTTModel):
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
@property
def delivery_price(self):
if not self.delivery_price_CN_RU and self.parent_id:
return self.parent.delivery_price
else:
return self.delivery_price_CN_RU
class UserQuerySet(models.QuerySet):
pass
class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError("The given email must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("job_title", User.ADMIN)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
return self._create_user(email, password, **extra_fields)
class User(AbstractUser):
ADMIN = "admin"
ORDER_MANAGER = "ordermanager"
PRODUCT_MANAGER = "productmanager"
JOB_CHOICES = (
(ADMIN, 'Администратор'),
(ORDER_MANAGER, 'Менеджер по заказам'),
(PRODUCT_MANAGER, 'Менеджер по закупкам'),
)
# Login by email
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
username = None
email = models.EmailField("Эл. почта", unique=True)
first_name = models.CharField(_("first name"), max_length=150, blank=True, null=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True, null=True)
middle_name = models.CharField("Отчество", max_length=150, blank=True, null=True)
job_title = models.CharField("Должность", max_length=30, choices=JOB_CHOICES)
objects = UserManager()
@property
def commission_price(self):
""" Get commission from object or from its parent """
if not self.commission and self.parent_id:
return self.parent.commission_price
else:
return self.commission
def is_superuser(self):
return self.job_title == self.ADMIN
@display(description='ФИО')
def full_name(self):
return concat_not_null_values(self.last_name, self.first_name, self.middle_name)
class PromocodeQuerySet(models.QuerySet):
@ -139,20 +190,17 @@ class Image(models.Model):
DEFAULT = 0
PREVIEW = 1
DOC = 2
GIFT = 3
TYPE_CHOICES = (
(DEFAULT, 'Изображение'),
(PREVIEW, 'Превью'),
(DOC, 'Документ'),
(GIFT, 'Подарок'),
)
TYPE_TO_UPLOAD_PATH = {
DEFAULT: 'checklist_images/',
PREVIEW: 'checklist_images/',
DOC: 'docs/',
GIFT: 'gifts/',
}
image = models.ImageField('Файл изображения', upload_to=image_upload_path)
@ -166,20 +214,6 @@ class Image(models.Model):
return f"{self.get_type_display()}: {getattr(self.image, 'name', '')}"
class Gift(models.Model):
name = models.CharField('Название', max_length=100)
image = models.ImageField('Фото', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.GIFT], null=True, blank=True)
min_price = models.DecimalField('Минимальная цена в юанях', help_text='от какой суммы доступен подарок', max_digits=10, decimal_places=2, default=0)
available_count = models.PositiveSmallIntegerField('Доступное количество', default=0)
def __str__(self):
return self.name
class Meta:
verbose_name = 'Подарок'
verbose_name_plural = 'Подарки'
def generate_checklist_id():
""" Generate unique id for Checklist """
@ -194,57 +228,23 @@ def generate_checklist_id():
class ChecklistQuerySet(models.QuerySet):
def with_base_related(self):
return self.select_related('manager', 'category', 'payment_method',
'promocode', 'price_snapshot', 'gift', 'customer') \
return self.select_related('manager', 'category', 'payment_method', 'promocode')\
.prefetch_related(Prefetch('images', to_attr='_images'))
def default_ordering(self):
return self.order_by(F('status_updated_at').desc(nulls_last=True))
def annotate_price_rub(self):
return self.annotate(
_yuan_rate=Case(
When(price_snapshot_id__isnull=False, then=F('price_snapshot__yuan_rate')),
default=GlobalSettings.load().full_yuan_rate
),
_price_rub=Ceil(F('_yuan_rate') * F('price_yuan'))
)
yuan_rate = GlobalSettings.load().yuan_rate
return self.annotate(_price_rub=F('price_yuan') * yuan_rate)
def annotate_commission_rub(self):
default_commission = GlobalSettings.load().commission_rub
over_150k_commission = F('_price_rub') * settings.COMMISSION_OVER_150K
category_commission_is_zero_and_parent_present = (
(Q(category__commission__isnull=True) | Q(category__commission=0)) & Q(category__parent__isnull=False)
)
return self.annotate(
_category_commission_percent=Case(
When(category_commission_is_zero_and_parent_present, then=F('category__parent__commission')),
default=F('category__commission')
),
_category_commission=F('_category_commission_percent') * F('_price_rub') / 100,
_over_150k_commission=Case(
When(GreaterThan(F("_price_rub"), 150_000), then=over_150k_commission),
default=0,
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()
),
_commission_rub=Case(
When(price_snapshot_id__isnull=False, then=F('price_snapshot__commission_rub')),
default=Max(default_commission, F('_over_150k_commission'), F('_category_commission')),
output_field=DecimalField()
),
)
class PriceSnapshot(models.Model):
yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0)
commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
))
@cleanup.select
@ -265,7 +265,6 @@ class Checklist(models.Model):
COMPLETED = "completed"
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
CHOICES = (
(DRAFT, 'Черновик'),
@ -282,63 +281,6 @@ class Checklist(models.Model):
(COMPLETED, 'Завершен'),
)
def get_tg_notification(self):
from tg_bot.messages import TGOrderStatusMessage as msg
match self.status:
case Checklist.Status.NEW:
return msg.NEW.format(order_id=self.id, order_link=self.order_link)
case Checklist.Status.BUYING:
if not self.is_split_payment:
return msg.BUYING_NON_SPLIT.format(order_id=self.id)
else:
return msg.BUYING_SPLIT.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay())
case Checklist.Status.BOUGHT:
return msg.BOUGHT.format(order_id=self.id)
case Checklist.Status.CHINA:
return msg.CHINA.format(order_id=self.id)
case Checklist.Status.CHINA_RUSSIA:
return msg.CHINA_RUSSIA.format(order_id=self.id)
case Checklist.Status.RUSSIA:
if self.delivery == Checklist.DeliveryType.PICKUP:
return msg.RUSSIA_PICKUP.format(order_id=self.id)
elif self.delivery in Checklist.DeliveryType.CDEK_TYPES:
return msg.RUSSIA_CDEK.format(order_id=self.id)
case Checklist.Status.SPLIT_WAITING:
if self.delivery == Checklist.DeliveryType.PICKUP:
return msg.SPLIT_WAITING_PICKUP.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay)
elif self.delivery in Checklist.DeliveryType.CDEK_TYPES:
return msg.SPLIT_WAITING_CDEK.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay)
# FIXME: split_accepted ?
case Checklist.Status.SPLIT_PAID:
return msg.SPLIT_PAID.format(order_id=self.id)
case Checklist.Status.CDEK:
return msg.CDEK.format(order_id=self.id)
case Checklist.Status.COMPLETED:
return msg.COMPLETED.format(order_id=self.id)
case _:
return None
@property
def order_link(self):
return f"https://poizonstore.com/orderpageinprogress/{self.id}"
def split_amount_to_pay(self):
# FIXME: it's stupid, create PaymentInfo model or something
return self.full_price // 2
# Delivery
class DeliveryType:
PICKUP = "pickup"
@ -351,16 +293,13 @@ class Checklist(models.Model):
(CDEK_COURIER, 'Курьерская доставка CDEK'),
)
CDEK_TYPES = (CDEK, CDEK_COURIER)
created_at = models.DateTimeField(auto_now_add=True)
status_updated_at = models.DateTimeField('Дата обновления статуса заказа', auto_now_add=True, editable=False)
status_updated_at = models.DateTimeField('Дата обновления статуса заказа', auto_now_add=True)
id = models.CharField(primary_key=True, max_length=settings.CHECKLIST_ID_LENGTH,
default=generate_checklist_id, editable=False)
status = models.CharField('Статус заказа', max_length=15, choices=Status.CHOICES, default=Status.NEW)
# managerid
manager = models.ForeignKey('account.User', verbose_name='Менеджер', related_name='manager_orders',
on_delete=models.SET_NULL, blank=True, null=True)
manager = models.ForeignKey('User', verbose_name='Менеджер', on_delete=models.SET_NULL, blank=True, null=True)
product_link = models.URLField('Ссылка на товар', null=True, blank=True)
category = models.ForeignKey('Category', verbose_name="Категория", blank=True, null=True, on_delete=models.SET_NULL)
@ -375,11 +314,14 @@ class Checklist(models.Model):
# promo
promocode = models.ForeignKey('Promocode', verbose_name='Промокод', on_delete=models.PROTECT, null=True, blank=True)
gift = models.ForeignKey('Gift', verbose_name='Подарок', on_delete=models.SET_NULL, null=True, blank=True)
comment = models.CharField('Комментарий', max_length=200, null=True, blank=True)
customer = models.ForeignKey('account.User', on_delete=models.CASCADE, related_name='customer_orders', blank=True,
null=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)
@ -399,10 +341,8 @@ class Checklist(models.Model):
split_payment_proof = models.ImageField('Подтверждение оплаты сплита',
upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC],
null=True, blank=True)
split_accepted = models.BooleanField('Сплит принят', default=False)
receipt = models.ImageField('Фото чека', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC], null=True,
blank=True) # checkphoto
receipt = models.ImageField('Фото чека', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC], null=True, blank=True) # checkphoto
delivery = models.CharField('Тип доставки', max_length=15, choices=DeliveryType.CHOICES, null=True, blank=True)
# trackid
@ -410,10 +350,6 @@ class Checklist(models.Model):
cdek_tracking = models.CharField('Трек-номер СДЭК', max_length=100, null=True, blank=True)
cdek_barcode_pdf = models.FileField('Штрих-код СДЭК в PDF', upload_to='docs', null=True, blank=True)
price_snapshot = models.ForeignKey('PriceSnapshot', verbose_name='Сохраненные цены',
related_name='checklist',
on_delete=models.SET_NULL, null=True, blank=True)
objects = ChecklistQuerySet.as_manager()
class Meta:
@ -432,17 +368,11 @@ class Checklist(models.Model):
@property
def price_rub(self) -> int:
# Prefer annotated field for calculation
# Prefer annotated field
if hasattr(self, '_price_rub'):
return self._price_rub
# Get saved prices
if self.price_snapshot_id:
yuan_rate = self.price_snapshot.yuan_rate
else:
yuan_rate = GlobalSettings.load().full_yuan_rate
return math.ceil(yuan_rate * self.price_yuan)
return math.ceil(GlobalSettings.load().yuan_rate * self.price_yuan)
@property
def full_price(self) -> int:
@ -462,36 +392,22 @@ class Checklist(models.Model):
no_comission = promocode.no_comission
if not free_delivery:
price += self.delivery_price_CN + self.delivery_price_CN_RU
price += GlobalSettings.load().delivery_price_CN + self.delivery_price_CN_RU
if not no_comission:
price += self.commission_rub
# Add commission of bottom-most category
if self.category:
category = self.category.get_ancestors(ascending=True, include_self=True).first()
category_commission = getattr(category, 'commission', 0)
price += category_commission * self.price_rub / 100
return max(0, math.ceil(price))
@property
def yuan_rate(self) -> Decimal:
# Get saved value if exists
if self.price_snapshot_id:
return self.price_snapshot.yuan_rate
else:
return GlobalSettings.load().full_yuan_rate
@property
def delivery_price_CN(self) -> Decimal:
# Get saved value if exists
if self.price_snapshot_id:
return self.price_snapshot.delivery_price_CN
else:
return GlobalSettings.load().delivery_price_CN
@property
def delivery_price_CN_RU(self) -> Decimal:
# Get saved value if exists
if self.price_snapshot_id:
return self.price_snapshot.delivery_price_CN_RU
else:
return getattr(self.category, 'delivery_price', Decimal(0))
return getattr(self.category, 'delivery_price_CN_RU', Decimal(0))
@property
def commission_rub(self) -> Decimal:
@ -499,22 +415,9 @@ class Checklist(models.Model):
if hasattr(self, '_commission_rub'):
return self._commission_rub
# Prefer saved value
if self.price_snapshot_id:
return self.price_snapshot.commission_rub
# Default commission
commission = GlobalSettings.load().commission_rub
if self.price_rub > 150_000:
commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))
if self.category_id:
# Add commission of bottom-most category
category_commission = getattr(self.category, 'commission_price', 0)
commission = max(commission, category_commission * self.price_rub / 100)
return commission
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):
@ -564,107 +467,23 @@ class Checklist(models.Model):
self.images.add(image_obj)
def save_prices(self):
# Temporarily remove snapshot from object
self.price_snapshot = None
snapshot, _ = PriceSnapshot.objects.get_or_create(
checklist__id=self.id,
defaults={
'yuan_rate': self.yuan_rate,
'delivery_price_CN': self.delivery_price_CN,
'delivery_price_CN_RU': self.delivery_price_CN_RU,
'commission_rub': self.commission_rub,
}
)
# Restore snapshot
self.price_snapshot = snapshot
def _notify_about_status_change(self):
if self.customer_id is None:
return
tg_message = self.get_tg_notification()
if tg_message:
self.customer.notify_tg_bot(tg_message)
def _check_eligible_for_order_bonus(self):
if self.customer_id is None:
return
if self.status != Checklist.Status.CHINA_RUSSIA:
return
# Check if any BonusProgramTransaction bound to current order exists
from account.models import BonusProgramTransaction
if BonusProgramTransaction.objects.filter(order_id=self.id).exists():
return
# Apply either referral bonus or order bonus, not both
if self.customer.inviter is not None and self.customer.customer_orders.count() == 1:
self.customer.add_referral_bonus(self, for_inviter=False)
self.customer.inviter.add_referral_bonus(self, for_inviter=True)
else:
self.customer.add_order_bonus(self)
# TODO: split into sub-functions
def save(self, *args, **kwargs):
if self.id:
old_obj = Checklist.objects.filter(id=self.id).first()
self._check_eligible_for_order_bonus()
# If status was updated, update status_updated_at field
if old_obj is not None and self.status != old_obj.status:
if old_obj and self.status != old_obj.status:
self.status_updated_at = timezone.now()
self._notify_about_status_change()
# TODO: remove bonuses if order is canceled?
# Invalidate old CDEK barcode PDF
if not self.cdek_barcode_pdf or self.cdek_tracking != old_obj.cdek_tracking:
self.cdek_barcode_pdf.delete(save=False)
self.cdek_barcode_pdf = None
# Try to get CDEK barcode PDF
if not self.cdek_barcode_pdf and self.cdek_tracking and self.status in Checklist.Status.PDF_AVAILABLE_STATUSES:
from store.views import CDEKAPI
pdf_file = CDEKAPI.client.get_barcode_file(self.cdek_tracking)
if pdf_file:
self.cdek_barcode_pdf.save(f'{self.id}_barcode.pdf', pdf_file, save=False)
# Invalidate old preview_image if full_price changed
price_changed = old_obj is not None and self.full_price != old_obj.full_price
if price_changed:
self.preview_image.delete(save=False)
self.cdek_barcode_pdf.save(f'{self.id}_barcode.pdf', pdf_file)
# Create preview image
if self.preview_image is None:
if not self.preview_image:
self.generate_preview()
# Update available gifts count
old_gift = getattr(old_obj, 'gift', None)
if self.gift != old_gift:
# Decrement new gift
if self.gift:
self.gift.available_count = max(0, self.gift.available_count - 1)
self.gift.save()
# Increment new gift
if old_gift:
old_gift.available_count = max(0, old_gift.available_count + 1)
old_gift.save()
# Save price details to snapshot
if self.price_snapshot_id:
# Status updated from other statuses back to DRAFT
if self.status == Checklist.Status.DRAFT:
self.price_snapshot.delete()
self.price_snapshot = None
elif self.status != Checklist.Status.DRAFT:
self.save_prices()
super().save(*args, **kwargs)

View File

@ -1,12 +1,22 @@
from drf_extra_fields.fields import Base64ImageField
from rest_framework import serializers
from account.serializers import UserSerializer
from utils.exceptions import CRMException
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image, Gift
from store.models import User, Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image
from store.utils import get_primary_key_related_model
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)
class Meta:
model = User
fields = ('id', 'login', 'job', 'name', 'lastname', 'surname',)
class ImageSerializer(serializers.ModelSerializer):
image = Base64ImageField()
@ -53,61 +63,50 @@ class CategoryFullSerializer(CategorySerializer):
fields = CategorySerializer.Meta.fields + ('children',)
class GiftSerializer(serializers.ModelSerializer):
image = Base64ImageField(required=False, allow_null=True)
class Meta:
model = Gift
fields = ('id', 'name', 'image', 'min_price', 'available_count')
class ChecklistSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True)
manager_id = serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
managerid = serializers.PrimaryKeyRelatedField(source='manager_id', read_only=True, allow_null=True)
link = serializers.URLField(source='product_link', required=False)
category = get_primary_key_related_model(CategoryChecklistSerializer, required=False, allow_null=True)
size = serializers.CharField(required=False, allow_null=True)
image = ImageListSerializer(source='main_images', required=False)
preview_image_url = serializers.ImageField(read_only=True)
previewimage = serializers.ImageField(source='preview_image_url', read_only=True)
promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(),
required=False, allow_null=True)
promo = serializers.SlugRelatedField(source='promocode', slug_field='name',
queryset=Promocode.objects.active(), required=False, allow_null=True)
gift = get_primary_key_related_model(GiftSerializer, required=False, allow_null=True)
currency = serializers.SerializerMethodField('get_yuan_rate')
curencycurency2 = serializers.DecimalField(source='price_yuan', required=False, max_digits=10, decimal_places=2)
currency3 = serializers.IntegerField(source='price_rub', read_only=True)
chinadelivery = serializers.SerializerMethodField('get_delivery_price_CN', read_only=True)
chinadelivery2 = serializers.DecimalField(source='delivery_price_CN_RU', read_only=True, max_digits=10,
decimal_places=2)
fullprice = serializers.IntegerField(source='full_price', read_only=True)
realprice = serializers.DecimalField(source='real_price', required=False, allow_null=True, max_digits=10,
decimal_places=2)
commission = serializers.SerializerMethodField('get_commission', read_only=True)
yuan_rate = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
price_yuan = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
price_rub = serializers.IntegerField(read_only=True)
buyername = serializers.CharField(source='buyer_name', required=False, allow_null=True)
buyerphone = serializers.CharField(source='buyer_phone', required=False, allow_null=True)
tg = serializers.CharField(source='buyer_telegram', required=False, allow_null=True)
delivery_price_CN = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
delivery_price_CN_RU = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
receivername = serializers.CharField(source='receiver_name', required=False, allow_null=True)
reveiverphone = serializers.CharField(source='receiver_phone', required=False, allow_null=True)
full_price = serializers.IntegerField(read_only=True)
real_price = serializers.DecimalField(required=False, allow_null=True, max_digits=10, decimal_places=2)
commission_rub = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
customer = get_primary_key_related_model(UserSerializer, required=False, allow_null=True)
receiver_name = serializers.CharField(required=False, allow_null=True)
receiver_phone = serializers.CharField(required=False, allow_null=True)
is_split_payment = serializers.BooleanField(required=False)
payment_method = serializers.SlugRelatedField(slug_field='slug',
split = serializers.BooleanField(source='is_split_payment', required=False)
paymenttype = serializers.SlugRelatedField(source='payment_method', slug_field='slug',
queryset=PaymentMethod.objects.all(),
required=False, allow_null=True)
payment_proof = Base64ImageField(required=False, allow_null=True)
paymentprovement = Base64ImageField(source='payment_proof', required=False, allow_null=True)
split_payment_proof = Base64ImageField(required=False, allow_null=True)
receipt = Base64ImageField(required=False, allow_null=True)
poizon_tracking = serializers.CharField(required=False, allow_null=True)
checkphoto = Base64ImageField(source='receipt', required=False, allow_null=True)
trackid = serializers.CharField(source='poizon_tracking', required=False, allow_null=True)
cdek_tracking = serializers.CharField(required=False, allow_null=True)
delivery = serializers.ChoiceField(choices=Checklist.DeliveryType.CHOICES, required=False, allow_null=True)
delivery_display = serializers.CharField(source='get_delivery_display', read_only=True)
created_at = serializers.DateTimeField(read_only=True)
status_updated_at = serializers.DateTimeField(read_only=True)
startDate = serializers.DateTimeField(source='created_at', read_only=True)
currentDate = serializers.DateTimeField(source='status_updated_at', read_only=True)
def _collect_images_by_fields(self, validated_data):
images = {}
@ -129,12 +128,6 @@ class ChecklistSerializer(serializers.ModelSerializer):
def create(self, validated_data):
images = self._collect_images_by_fields(validated_data)
# Managers can create orders with arbitrary customers
# Client orders are created with client's account
user = self.context['request'].user
if not user.is_manager or validated_data.get('customer') is None:
validated_data['customer'] = user
instance = super().create(validated_data)
self._create_main_images(instance, images.get('main_images'))
return instance
@ -158,91 +151,61 @@ class ChecklistSerializer(serializers.ModelSerializer):
return instance
@staticmethod
def get_yuan_rate(obj: Checklist):
return GlobalSettings.load().yuan_rate
@staticmethod
def get_image(obj: Checklist):
return obj.images.all()
@staticmethod
def get_delivery_price_CN(obj: Checklist):
return GlobalSettings.load().delivery_price_CN
@staticmethod
def get_commission(obj: Checklist):
return GlobalSettings.load().commission_rub
class Meta:
model = Checklist
fields = ('id', 'status', 'manager_id', 'link',
fields = ('id', 'status', 'managerid', 'link',
'category',
'brand', 'model', 'size',
'image',
'preview_image_url',
'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub',
'promocode', 'gift',
'previewimage',
'currency', 'curencycurency2', 'currency3', 'chinadelivery', 'chinadelivery2', 'commission',
'promo',
'comment',
'full_price', 'real_price',
'customer',
'receiver_name', 'receiver_phone',
'is_split_payment', 'payment_method', 'payment_proof', 'split_payment_proof', 'split_accepted', 'receipt',
'poizon_tracking', 'cdek_tracking', 'cdek_barcode_pdf', 'delivery', 'delivery_display',
'created_at', 'status_updated_at', 'buy_time_remaining'
'fullprice', 'realprice',
'buyername', 'buyerphone', 'tg',
'receivername', 'reveiverphone',
'split', 'paymenttype', 'paymentprovement', 'split_payment_proof', 'checkphoto',
'trackid', 'cdek_tracking', 'cdek_barcode_pdf', 'delivery', 'delivery_display',
'startDate', 'currentDate', 'buy_time_remaining'
)
class ClientChecklistSerializerMixin:
def validate(self, attrs):
gift = attrs.get('gift')
if gift is not None:
if self.instance.price_yuan < gift.min_price:
raise CRMException("Can't add gift: price of order < min_price of gift")
if gift.available_count == 0:
raise CRMException("Gift is not available")
return attrs
class ClientCreateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistSerializer):
status = serializers.SerializerMethodField()
class AnonymousUserChecklistSerializer(ChecklistSerializer):
class Meta:
model = ChecklistSerializer.Meta.model
fields = ChecklistSerializer.Meta.fields
writable_fields = {
'link',
'brand', 'model', 'size', 'category',
'price_yuan',
'comment',
}
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)
def get_status(self, obj):
return Checklist.Status.DRAFT
class ClientUpdateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistSerializer):
class Meta:
model = ChecklistSerializer.Meta.model
fields = ChecklistSerializer.Meta.fields
writable_fields = {
'comment',
'payment_proof', 'payment_method',
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) -
{'paymentprovement', 'paymenttype',
'buyername', 'buyerphone',
'delivery',
'receiver_name', 'receiver_phone',
'gift', 'cdek_barcode_pdf'
}
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)
'recievername', 'recieverphone', 'tg'})
class GlobalSettingsSerializer(serializers.ModelSerializer):
currency = serializers.DecimalField(source='full_yuan_rate', read_only=True, max_digits=10, decimal_places=2)
yuan_rate_last_updated = serializers.DateTimeField(read_only=True)
currency = serializers.DecimalField(source='yuan_rate', max_digits=10, decimal_places=2)
chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2)
commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2)
pickup = serializers.CharField(source='pickup_address')
class Meta:
model = GlobalSettings
fields = ('currency', 'yuan_rate_last_updated', 'yuan_rate_commission', 'commission', 'chinadelivery', 'pickup', 'time_to_buy')
class AnonymousGlobalSettingsSerializer(GlobalSettingsSerializer):
class Meta:
model = GlobalSettingsSerializer.Meta.model
fields = tuple(set(GlobalSettingsSerializer.Meta.fields) - {'yuan_rate_commission', 'yuan_rate_last_updated'})
fields = ('currency', 'commission', 'chinadelivery', 'pickup', 'time_to_buy')
class PaymentMethodSerializer(serializers.ModelSerializer):

Some files were not shown because too many files have changed in this diff Show More