refactor: Заменить сущность Магазин (Shop) на Склад (Warehouse)
Упрощена логика системы путём замены отдельной сущности "Магазин" на универсальную сущность "Склад", которая может использоваться как точка самовывоза. Изменения: - Расширена модель Warehouse: добавлены адрес, контакты, флаг is_pickup_point - Модель Order: поле pickup_shop заменено на pickup_warehouse - Обновлены все формы, сервисы, views, admin для работы со складами - Обновлены шаблоны HTML и JavaScript код - Удалено приложение shops полностью - Пересозданы миграции БД - Обновлён навбар (удалена ссылка на магазины) Преимущества: - Упрощена архитектура системы - Единая точка управления складами и точками самовывоза - Интеграция с системой инвентаризации 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
|
import phonenumber_field.modelfields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -216,8 +217,13 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=200, verbose_name='Название')),
|
('name', models.CharField(max_length=200, verbose_name='Название')),
|
||||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||||
|
('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица')),
|
||||||
|
('building_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания')),
|
||||||
|
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region='BY', verbose_name='Телефон')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
||||||
('is_default', models.BooleanField(default=False, help_text='Автоматически выбирается при создании новых документов', verbose_name='Склад по умолчанию')),
|
('is_default', models.BooleanField(default=False, help_text='Автоматически выбирается при создании новых документов', verbose_name='Склад по умолчанию')),
|
||||||
|
('is_pickup_point', models.BooleanField(default=True, help_text='Можно ли выбрать этот склад как точку самовывоза заказа', verbose_name='Доступен для самовывоза')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -128,6 +128,10 @@ class Migration(migrations.Migration):
|
|||||||
model_name='warehouse',
|
model_name='warehouse',
|
||||||
index=models.Index(fields=['is_default'], name='inventory_w_is_defa_4b7615_idx'),
|
index=models.Index(fields=['is_default'], name='inventory_w_is_defa_4b7615_idx'),
|
||||||
),
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='warehouse',
|
||||||
|
index=models.Index(fields=['is_pickup_point'], name='inventory_w_is_pick_e86268_idx'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='transferbatch',
|
model_name='transferbatch',
|
||||||
name='from_warehouse',
|
name='from_warehouse',
|
||||||
|
|||||||
@@ -3,20 +3,38 @@ from django.utils import timezone
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
|
|
||||||
class Warehouse(models.Model):
|
class Warehouse(models.Model):
|
||||||
"""
|
"""
|
||||||
Склад (физическое или логическое место хранения).
|
Склад (физическое или логическое место хранения).
|
||||||
|
Может использоваться как точка самовывоза для заказов.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=200, verbose_name="Название")
|
name = models.CharField(max_length=200, verbose_name="Название")
|
||||||
description = models.TextField(blank=True, null=True, verbose_name="Описание")
|
description = models.TextField(blank=True, null=True, verbose_name="Описание")
|
||||||
|
|
||||||
|
# Адрес
|
||||||
|
street = models.CharField(max_length=255, blank=True, null=True, verbose_name="Улица")
|
||||||
|
building_number = models.CharField(max_length=20, blank=True, null=True, verbose_name="Номер здания")
|
||||||
|
|
||||||
|
# Контакты
|
||||||
|
phone = PhoneNumberField(region='BY', blank=True, null=True, verbose_name="Телефон")
|
||||||
|
email = models.EmailField(blank=True, null=True, verbose_name="Email")
|
||||||
|
|
||||||
|
# Настройки
|
||||||
is_active = models.BooleanField(default=True, verbose_name="Активен")
|
is_active = models.BooleanField(default=True, verbose_name="Активен")
|
||||||
is_default = models.BooleanField(
|
is_default = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name="Склад по умолчанию",
|
verbose_name="Склад по умолчанию",
|
||||||
help_text="Автоматически выбирается при создании новых документов"
|
help_text="Автоматически выбирается при создании новых документов"
|
||||||
)
|
)
|
||||||
|
is_pickup_point = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name="Доступен для самовывоза",
|
||||||
|
help_text="Можно ли выбрать этот склад как точку самовывоза заказа"
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
|
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
|
||||||
|
|
||||||
@@ -26,11 +44,24 @@ class Warehouse(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['is_active']),
|
models.Index(fields=['is_active']),
|
||||||
models.Index(fields=['is_default']),
|
models.Index(fields=['is_default']),
|
||||||
|
models.Index(fields=['is_pickup_point']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.street and self.building_number:
|
||||||
|
return f"{self.name} ({self.street}, {self.building_number})"
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_address(self):
|
||||||
|
"""Полный адрес склада"""
|
||||||
|
parts = []
|
||||||
|
if self.street:
|
||||||
|
parts.append(self.street)
|
||||||
|
if self.building_number:
|
||||||
|
parts.append(self.building_number)
|
||||||
|
return ', '.join(parts) if parts else "Адрес не указан"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Обеспечиваем что только один склад может быть по умолчанию в рамках одного тенанта"""
|
"""Обеспечиваем что только один склад может быть по умолчанию в рамках одного тенанта"""
|
||||||
if self.is_default:
|
if self.is_default:
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ TENANT_APPS = [
|
|||||||
'nested_admin',
|
'nested_admin',
|
||||||
'django_filters', # Фильтрация данных
|
'django_filters', # Фильтрация данных
|
||||||
'customers', # Клиенты магазина
|
'customers', # Клиенты магазина
|
||||||
'shops', # Точки магазина/самовывоза
|
|
||||||
'products', # Товары и категории
|
'products', # Товары и категории
|
||||||
'orders', # Заказы
|
'orders', # Заказы
|
||||||
'inventory', # Складской учет
|
'inventory', # Складской учет
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ urlpatterns = [
|
|||||||
path('customers/', include('customers.urls')), # Управление клиентами
|
path('customers/', include('customers.urls')), # Управление клиентами
|
||||||
path('inventory/', include('inventory.urls')), # Управление складом
|
path('inventory/', include('inventory.urls')), # Управление складом
|
||||||
path('orders/', include('orders.urls')), # Управление заказами
|
path('orders/', include('orders.urls')), # Управление заказами
|
||||||
path('shops/', include('shops.urls')), # Управление магазинами
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Serve media files during development
|
# Serve media files during development
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class OrderAdmin(admin.ModelAdmin):
|
|||||||
'is_delivery',
|
'is_delivery',
|
||||||
'customer_is_recipient',
|
'customer_is_recipient',
|
||||||
'delivery_address',
|
'delivery_address',
|
||||||
'pickup_shop',
|
'pickup_warehouse',
|
||||||
'delivery_date',
|
'delivery_date',
|
||||||
'delivery_time_start',
|
'delivery_time_start',
|
||||||
'delivery_time_end',
|
'delivery_time_end',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from django import forms
|
|||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
from .models import Order, OrderItem, Address, OrderStatus
|
from .models import Order, OrderItem, Address, OrderStatus
|
||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
from shops.models import Shop
|
from inventory.models import Warehouse
|
||||||
from products.models import Product, ProductKit
|
from products.models import Product, ProductKit
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class OrderForm(forms.ModelForm):
|
|||||||
'customer',
|
'customer',
|
||||||
'is_delivery',
|
'is_delivery',
|
||||||
'delivery_address',
|
'delivery_address',
|
||||||
'pickup_shop',
|
'pickup_warehouse',
|
||||||
'delivery_date',
|
'delivery_date',
|
||||||
'delivery_time_start',
|
'delivery_time_start',
|
||||||
'delivery_time_end',
|
'delivery_time_end',
|
||||||
@@ -145,11 +145,11 @@ class OrderForm(forms.ModelForm):
|
|||||||
})
|
})
|
||||||
self.fields['delivery_address'].required = False
|
self.fields['delivery_address'].required = False
|
||||||
|
|
||||||
self.fields['pickup_shop'].widget.attrs.update({
|
self.fields['pickup_warehouse'].widget.attrs.update({
|
||||||
'class': 'form-select select2',
|
'class': 'form-select select2',
|
||||||
'data-placeholder': 'Выберите точку самовывоза'
|
'data-placeholder': 'Выберите склад для самовывоза'
|
||||||
})
|
})
|
||||||
self.fields['pickup_shop'].required = False
|
self.fields['pickup_warehouse'].required = False
|
||||||
|
|
||||||
# Опциональные поля даты/времени
|
# Опциональные поля даты/времени
|
||||||
self.fields['delivery_date'].required = False
|
self.fields['delivery_date'].required = False
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from orders.models import Order, OrderItem, Address
|
from orders.models import Order, OrderItem, Address
|
||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
from shops.models import Shop
|
from inventory.models import Warehouse
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
|
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ class Command(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
addresses = list(Address.objects.all())
|
addresses = list(Address.objects.all())
|
||||||
shops = list(Shop.objects.all())
|
warehouses = list(Warehouse.objects.filter(is_pickup_point=True))
|
||||||
|
|
||||||
if not addresses and not shops:
|
if not addresses and not warehouses:
|
||||||
self.stdout.write(self.style.ERROR('Нет ни адресов, ни магазинов! Создайте хотя бы что-то одно.'))
|
self.stdout.write(self.style.ERROR('Нет ни адресов, ни складов для самовывоза! Создайте хотя бы что-то одно.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Статусы и их вероятности
|
# Статусы и их вероятности
|
||||||
@@ -111,8 +111,8 @@ class Command(BaseCommand):
|
|||||||
# Для доставки выбираем случайный адрес (адреса теперь привязаны к заказам)
|
# Для доставки выбираем случайный адрес (адреса теперь привязаны к заказам)
|
||||||
order.delivery_address = random.choice(addresses)
|
order.delivery_address = random.choice(addresses)
|
||||||
order.delivery_cost = Decimal(random.randint(200, 500))
|
order.delivery_cost = Decimal(random.randint(200, 500))
|
||||||
elif shops:
|
elif warehouses:
|
||||||
order.pickup_shop = random.choice(shops)
|
order.pickup_warehouse = random.choice(warehouses)
|
||||||
order.delivery_cost = Decimal(0)
|
order.delivery_cost = Decimal(0)
|
||||||
|
|
||||||
# Дата и время
|
# Дата и время
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import simple_history.models
|
import simple_history.models
|
||||||
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('customers', '0001_initial'),
|
('customers', '0001_initial'),
|
||||||
('shops', '0001_initial'),
|
('inventory', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ class Migration(migrations.Migration):
|
|||||||
('code', models.SlugField(help_text="Уникальный идентификатор (например: 'completed', 'cancelled')", unique=True, verbose_name='Код статуса')),
|
('code', models.SlugField(help_text="Уникальный идентификатор (например: 'completed', 'cancelled')", unique=True, verbose_name='Код статуса')),
|
||||||
('label', models.CharField(blank=True, max_length=100, verbose_name='Метка для отображения')),
|
('label', models.CharField(blank=True, max_length=100, verbose_name='Метка для отображения')),
|
||||||
('is_system', models.BooleanField(default=False, help_text='True для встроенных статусов (draft, completed, cancelled)', verbose_name='Системный статус')),
|
('is_system', models.BooleanField(default=False, help_text='True для встроенных статусов (draft, completed, cancelled)', verbose_name='Системный статус')),
|
||||||
('is_positive_end', models.BooleanField(default=False, help_text='True если это финальный успешный статус (Выполнен)', verbose_name='Положительный конец')),
|
('is_positive_end', models.BooleanField(default=False, help_text='True если это финальный успешный статус (Выполнен)', verbose_name='Положительный исход сделки')),
|
||||||
('is_negative_end', models.BooleanField(default=False, help_text='True если это финальный отрицательный статус (Отменен)', verbose_name='Отрицательный конец')),
|
('is_negative_end', models.BooleanField(default=False, help_text='True если это финальный отрицательный статус (Отменен)', verbose_name='Отрицательный исход сделки')),
|
||||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
|
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
|
||||||
('color', models.CharField(blank=True, default='#808080', help_text='Например: #FF5733', max_length=7, verbose_name='Цвет (hex)')),
|
('color', models.CharField(blank=True, default='#808080', help_text='Например: #FF5733', max_length=7, verbose_name='Цвет (hex)')),
|
||||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||||
@@ -112,7 +112,7 @@ class Migration(migrations.Migration):
|
|||||||
('delivery_address', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.address', verbose_name='Адрес доставки')),
|
('delivery_address', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.address', verbose_name='Адрес доставки')),
|
||||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
('modified_by', models.ForeignKey(blank=True, db_constraint=False, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
|
('modified_by', models.ForeignKey(blank=True, db_constraint=False, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
|
||||||
('pickup_shop', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='shops.shop', verbose_name='Точка самовывоза')),
|
('pickup_warehouse', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='inventory.warehouse', verbose_name='Склад для самовывоза')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'historical Заказ',
|
'verbose_name': 'historical Заказ',
|
||||||
@@ -151,7 +151,7 @@ class Migration(migrations.Migration):
|
|||||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='customers.customer', verbose_name='Клиент')),
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='customers.customer', verbose_name='Клиент')),
|
||||||
('delivery_address', models.OneToOneField(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='orders.address', verbose_name='Адрес доставки')),
|
('delivery_address', models.OneToOneField(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='orders.address', verbose_name='Адрес доставки')),
|
||||||
('modified_by', models.ForeignKey(blank=True, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_orders', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
|
('modified_by', models.ForeignKey(blank=True, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_orders', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
|
||||||
('pickup_shop', models.ForeignKey(blank=True, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pickup_orders', to='shops.shop', verbose_name='Точка самовывоза')),
|
('pickup_warehouse', models.ForeignKey(blank=True, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pickup_orders', to='inventory.warehouse', verbose_name='Склад для самовывоза')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Заказ',
|
'verbose_name': 'Заказ',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 14:19
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def update_status_names_to_russian(apps, schema_editor):
|
|
||||||
"""Обновляем названия статусов на русский язык"""
|
|
||||||
OrderStatus = apps.get_model('orders', 'OrderStatus')
|
|
||||||
|
|
||||||
status_translations = {
|
|
||||||
'draft': 'Черновик',
|
|
||||||
'new': 'Новый',
|
|
||||||
'confirmed': 'Подтвережден',
|
|
||||||
'in_assembly': 'В сборке',
|
|
||||||
'in_delivery': 'В доставке',
|
|
||||||
'completed': 'Выполнен',
|
|
||||||
'return': 'Возврат',
|
|
||||||
'cancelled': 'Отменен',
|
|
||||||
}
|
|
||||||
|
|
||||||
for code, russian_name in status_translations.items():
|
|
||||||
try:
|
|
||||||
status = OrderStatus.objects.get(code=code)
|
|
||||||
status.name = russian_name
|
|
||||||
status.label = russian_name
|
|
||||||
status.save()
|
|
||||||
except OrderStatus.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_status_names(apps, schema_editor):
|
|
||||||
"""Откатываем названия статусов обратно на английский (для отката миграции)"""
|
|
||||||
OrderStatus = apps.get_model('orders', 'OrderStatus')
|
|
||||||
|
|
||||||
status_translations = {
|
|
||||||
'draft': 'Draft',
|
|
||||||
'new': 'New',
|
|
||||||
'confirmed': 'Confirmed',
|
|
||||||
'in_assembly': 'In Assembly',
|
|
||||||
'in_delivery': 'In Delivery',
|
|
||||||
'completed': 'Completed',
|
|
||||||
'return': 'Return',
|
|
||||||
'cancelled': 'Cancelled',
|
|
||||||
}
|
|
||||||
|
|
||||||
for code, english_name in status_translations.items():
|
|
||||||
try:
|
|
||||||
status = OrderStatus.objects.get(code=code)
|
|
||||||
status.name = english_name
|
|
||||||
status.label = english_name
|
|
||||||
status.save()
|
|
||||||
except OrderStatus.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('orders', '0002_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(update_status_names_to_russian, reverse_status_names),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 18:51
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('orders', '0003_update_status_names_to_russian'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderstatus',
|
|
||||||
name='is_negative_end',
|
|
||||||
field=models.BooleanField(default=False, help_text='True если это финальный отрицательный статус (Отменен)', verbose_name='Отрицательный исход сделки'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderstatus',
|
|
||||||
name='is_positive_end',
|
|
||||||
field=models.BooleanField(default=False, help_text='True если это финальный успешный статус (Выполнен)', verbose_name='Положительный исход сделки'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
from products.models import Product, ProductKit
|
from products.models import Product, ProductKit
|
||||||
from shops.models import Shop
|
from inventory.models import Warehouse
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
|
|
||||||
@@ -283,14 +283,14 @@ class Order(models.Model):
|
|||||||
help_text="Обязательно для курьерской доставки"
|
help_text="Обязательно для курьерской доставки"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Пункт самовывоза (для самовывоза)
|
# Склад для самовывоза
|
||||||
pickup_shop = models.ForeignKey(
|
pickup_warehouse = models.ForeignKey(
|
||||||
Shop,
|
Warehouse,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='pickup_orders',
|
related_name='pickup_orders',
|
||||||
verbose_name="Точка самовывоза",
|
verbose_name="Склад для самовывоза",
|
||||||
help_text="Обязательно для самовывоза"
|
help_text="Обязательно для самовывоза"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -511,10 +511,10 @@ class Order(models.Model):
|
|||||||
'delivery_address': 'Для доставки необходимо указать адрес доставки'
|
'delivery_address': 'Для доставки необходимо указать адрес доставки'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Проверка: для самовывоза обязателен пункт самовывоза
|
# Проверка: для самовывоза обязателен склад
|
||||||
if not self.is_delivery and not self.pickup_shop:
|
if not self.is_delivery and not self.pickup_warehouse:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'pickup_shop': 'Для самовывоза необходимо выбрать точку самовывоза'
|
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Проверка: время окончания должно быть позже времени начала
|
# Проверка: время окончания должно быть позже времени начала
|
||||||
@@ -618,9 +618,9 @@ class Order(models.Model):
|
|||||||
return f"Доставка по адресу: {self.delivery_address.full_address}"
|
return f"Доставка по адресу: {self.delivery_address.full_address}"
|
||||||
return "Доставка (адрес не указан)"
|
return "Доставка (адрес не указан)"
|
||||||
else:
|
else:
|
||||||
if self.pickup_shop:
|
if self.pickup_warehouse:
|
||||||
return f"Самовывоз из: {self.pickup_shop.name}"
|
return f"Самовывоз со склада: {self.pickup_warehouse.name}"
|
||||||
return "Самовывоз (точка не указана)"
|
return "Самовывоз (склад не указан)"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def delivery_time_window(self):
|
def delivery_time_window(self):
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class DraftOrderService:
|
|||||||
modified_by=user,
|
modified_by=user,
|
||||||
is_delivery=data.get('is_delivery', True),
|
is_delivery=data.get('is_delivery', True),
|
||||||
delivery_address=data.get('delivery_address'),
|
delivery_address=data.get('delivery_address'),
|
||||||
pickup_shop=data.get('pickup_shop'),
|
pickup_warehouse=data.get('pickup_warehouse'),
|
||||||
delivery_date=data.get('delivery_date'),
|
delivery_date=data.get('delivery_date'),
|
||||||
delivery_time_start=data.get('delivery_time_start'),
|
delivery_time_start=data.get('delivery_time_start'),
|
||||||
delivery_time_end=data.get('delivery_time_end'),
|
delivery_time_end=data.get('delivery_time_end'),
|
||||||
@@ -97,7 +97,7 @@ class DraftOrderService:
|
|||||||
# ForeignKey поля требуют специальной обработки
|
# ForeignKey поля требуют специальной обработки
|
||||||
fk_fields = {
|
fk_fields = {
|
||||||
'customer': 'customers.Customer',
|
'customer': 'customers.Customer',
|
||||||
'pickup_shop': 'shops.Shop',
|
'pickup_warehouse': 'inventory.Warehouse',
|
||||||
'status': 'orders.OrderStatus',
|
'status': 'orders.OrderStatus',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ class DraftOrderService:
|
|||||||
drafts = Order.objects.filter(
|
drafts = Order.objects.filter(
|
||||||
status__code='draft',
|
status__code='draft',
|
||||||
modified_by=user
|
modified_by=user
|
||||||
).select_related('customer', 'delivery_address', 'pickup_shop')
|
).select_related('customer', 'delivery_address', 'pickup_warehouse')
|
||||||
|
|
||||||
if customer:
|
if customer:
|
||||||
drafts = drafts.filter(customer=customer)
|
drafts = drafts.filter(customer=customer)
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
'input[type="checkbox"]',
|
'input[type="checkbox"]',
|
||||||
'input[type="radio"]',
|
'input[type="radio"]',
|
||||||
'select[name="delivery_address"]',
|
'select[name="delivery_address"]',
|
||||||
'select[name="pickup_shop"]',
|
'select[name="pickup_warehouse"]',
|
||||||
// Поля адреса доставки
|
// Поля адреса доставки
|
||||||
'input[name="address_street"]',
|
'input[name="address_street"]',
|
||||||
'input[name="address_building_number"]',
|
'input[name="address_building_number"]',
|
||||||
@@ -364,9 +364,9 @@
|
|||||||
data.delivery_address = parseInt(deliveryAddressField.value);
|
data.delivery_address = parseInt(deliveryAddressField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickupShopField = form.querySelector('select[name="pickup_shop"]');
|
const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
|
||||||
if (pickupShopField && pickupShopField.value) {
|
if (pickupWarehouseField && pickupWarehouseField.value) {
|
||||||
data.pickup_shop = parseInt(pickupShopField.value);
|
data.pickup_warehouse = parseInt(pickupWarehouseField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Поля адреса доставки (новая логика с прямым вводом)
|
// Поля адреса доставки (новая логика с прямым вводом)
|
||||||
|
|||||||
@@ -236,9 +236,9 @@
|
|||||||
data.delivery_address = parseInt(deliveryAddressField.value);
|
data.delivery_address = parseInt(deliveryAddressField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickupShopField = form.querySelector('select[name="pickup_shop"]');
|
const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
|
||||||
if (pickupShopField && pickupShopField.value) {
|
if (pickupWarehouseField && pickupWarehouseField.value) {
|
||||||
data.pickup_shop = parseInt(pickupShopField.value);
|
data.pickup_warehouse = parseInt(pickupWarehouseField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Новая логика выбора адреса
|
// Новая логика выбора адреса
|
||||||
|
|||||||
@@ -130,13 +130,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Точка самовывоза:</strong></div>
|
<div class="col-md-4"><strong>Склад для самовывоза:</strong></div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% if order.pickup_shop %}
|
{% if order.pickup_warehouse %}
|
||||||
{{ order.pickup_shop.name }}<br>
|
{{ order.pickup_warehouse.name }}<br>
|
||||||
<small class="text-muted">{{ order.pickup_shop.address }}</small>
|
<small class="text-muted">{{ order.pickup_warehouse.full_address }}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-danger">Не указана</span>
|
<span class="text-danger">Не указан</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -522,12 +522,12 @@
|
|||||||
<div class="row" id="pickup-fields" style="display: none;">
|
<div class="row" id="pickup-fields" style="display: none;">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.pickup_shop.id_for_label }}" class="form-label">
|
<label for="{{ form.pickup_warehouse.id_for_label }}" class="form-label">
|
||||||
Точка самовывоза
|
Склад для самовывоза
|
||||||
</label>
|
</label>
|
||||||
{{ form.pickup_shop }}
|
{{ form.pickup_warehouse }}
|
||||||
{% if form.pickup_shop.errors %}
|
{% if form.pickup_warehouse.errors %}
|
||||||
<div class="text-danger">{{ form.pickup_shop.errors }}</div>
|
<div class="text-danger">{{ form.pickup_warehouse.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def order_list(request):
|
|||||||
"""
|
"""
|
||||||
# Базовый queryset с оптимизацией запросов
|
# Базовый queryset с оптимизацией запросов
|
||||||
orders = Order.objects.select_related(
|
orders = Order.objects.select_related(
|
||||||
'customer', 'delivery_address', 'pickup_shop'
|
'customer', 'delivery_address', 'pickup_warehouse'
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Применяем фильтры через django-filter
|
# Применяем фильтры через django-filter
|
||||||
@@ -48,7 +48,7 @@ def order_list(request):
|
|||||||
def order_detail(request, pk):
|
def order_detail(request, pk):
|
||||||
"""Детальная информация о заказе"""
|
"""Детальная информация о заказе"""
|
||||||
order = get_object_or_404(
|
order = get_object_or_404(
|
||||||
Order.objects.select_related('customer', 'delivery_address', 'pickup_shop', 'modified_by')
|
Order.objects.select_related('customer', 'delivery_address', 'pickup_warehouse', 'modified_by')
|
||||||
.prefetch_related('items__product', 'items__product_kit', 'payments__created_by'),
|
.prefetch_related('items__product', 'items__product_kit', 'payments__created_by'),
|
||||||
pk=pk
|
pk=pk
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from .models import Shop
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Shop)
|
|
||||||
class ShopAdmin(admin.ModelAdmin):
|
|
||||||
"""
|
|
||||||
Админ-панель для управления магазинами/пунктами самовывоза.
|
|
||||||
"""
|
|
||||||
list_display = [
|
|
||||||
'name',
|
|
||||||
'full_address',
|
|
||||||
'phone',
|
|
||||||
'is_active',
|
|
||||||
'is_pickup_point',
|
|
||||||
]
|
|
||||||
|
|
||||||
list_filter = [
|
|
||||||
'is_active',
|
|
||||||
'is_pickup_point',
|
|
||||||
]
|
|
||||||
|
|
||||||
search_fields = [
|
|
||||||
'name',
|
|
||||||
'street',
|
|
||||||
'building_number',
|
|
||||||
'phone',
|
|
||||||
'email',
|
|
||||||
]
|
|
||||||
|
|
||||||
fieldsets = (
|
|
||||||
('Основная информация', {
|
|
||||||
'fields': ('name', 'description')
|
|
||||||
}),
|
|
||||||
('Адрес', {
|
|
||||||
'fields': ('street', 'building_number')
|
|
||||||
}),
|
|
||||||
('Контакты', {
|
|
||||||
'fields': ('phone', 'email')
|
|
||||||
}),
|
|
||||||
('Настройки', {
|
|
||||||
'fields': ('is_active', 'is_pickup_point')
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly_fields = ['created_at', 'updated_at']
|
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
|
||||||
"""Добавляем временные метки для существующих объектов"""
|
|
||||||
fieldsets = super().get_fieldsets(request, obj)
|
|
||||||
if obj: # Если объект уже существует
|
|
||||||
fieldsets = fieldsets + (
|
|
||||||
('Системная информация', {
|
|
||||||
'fields': ('created_at', 'updated_at'),
|
|
||||||
'classes': ('collapse',)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return fieldsets
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ShopsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'shops'
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from phonenumber_field.formfields import PhoneNumberField
|
|
||||||
from .models import Shop
|
|
||||||
|
|
||||||
|
|
||||||
class ShopForm(forms.ModelForm):
|
|
||||||
phone = PhoneNumberField(
|
|
||||||
region='BY',
|
|
||||||
required=False,
|
|
||||||
help_text='Формат: +375XXXXXXXXX или 80XXXXXXXXX',
|
|
||||||
widget=forms.TextInput(attrs={'placeholder': '+375XXXXXXXXX'})
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Shop
|
|
||||||
fields = [
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'street',
|
|
||||||
'building_number',
|
|
||||||
'phone',
|
|
||||||
'email',
|
|
||||||
'is_active',
|
|
||||||
'is_pickup_point',
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
'description': forms.Textarea(attrs={'rows': 3}),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
# Ensure phone displays in E.164 format
|
|
||||||
if self.instance and self.instance.phone:
|
|
||||||
self.initial['phone'] = str(self.instance.phone)
|
|
||||||
|
|
||||||
# Mark name field as required with label
|
|
||||||
self.fields['name'].label = 'Название магазина *'
|
|
||||||
self.fields['name'].required = True
|
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
if field_name == 'description':
|
|
||||||
# Textarea already has rows=3 from widget, just add class
|
|
||||||
field.widget.attrs.update({'class': 'form-control'})
|
|
||||||
elif field_name in ['is_active', 'is_pickup_point']:
|
|
||||||
# Checkbox fields need form-check-input class
|
|
||||||
field.widget.attrs.update({'class': 'form-check-input'})
|
|
||||||
elif field_name == 'phone':
|
|
||||||
# Phone field gets form-control class
|
|
||||||
field.widget.attrs.update({'class': 'form-control'})
|
|
||||||
else:
|
|
||||||
# Regular input fields get form-control class
|
|
||||||
field.widget.attrs.update({'class': 'form-control'})
|
|
||||||
|
|
||||||
# Add required attribute to HTML for name field
|
|
||||||
if field_name == 'name':
|
|
||||||
field.widget.attrs.update({'required': 'required'})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
|
||||||
|
|
||||||
import phonenumber_field.modelfields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Shop',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=200, verbose_name='Название магазина')),
|
|
||||||
('description', models.TextField(blank=True, help_text='Дополнительная информация о магазине', null=True, verbose_name='Описание')),
|
|
||||||
('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица')),
|
|
||||||
('building_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания')),
|
|
||||||
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Контактный телефон магазина', max_length=128, null=True, region=None, verbose_name='Телефон')),
|
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Работает ли магазин в данный момент', verbose_name='Активен')),
|
|
||||||
('is_pickup_point', models.BooleanField(default=True, help_text='Доступен ли магазин для самовывоза заказов', verbose_name='Пункт самовывоза')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Магазин',
|
|
||||||
'verbose_name_plural': 'Магазины',
|
|
||||||
'ordering': ['name'],
|
|
||||||
'indexes': [models.Index(fields=['is_active'], name='shops_shop_is_acti_bbb154_idx'), models.Index(fields=['is_pickup_point'], name='shops_shop_is_pick_a04981_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
|
||||||
|
|
||||||
|
|
||||||
class Shop(models.Model):
|
|
||||||
"""
|
|
||||||
Модель магазина/пункта самовывоза для цветочного магазина.
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=200,
|
|
||||||
verbose_name="Название магазина"
|
|
||||||
)
|
|
||||||
|
|
||||||
description = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Описание",
|
|
||||||
help_text="Дополнительная информация о магазине"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Адрес магазина
|
|
||||||
street = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Улица"
|
|
||||||
)
|
|
||||||
|
|
||||||
building_number = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Номер здания"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Контактная информация
|
|
||||||
phone = PhoneNumberField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Телефон",
|
|
||||||
help_text="Контактный телефон магазина"
|
|
||||||
)
|
|
||||||
|
|
||||||
email = models.EmailField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Email"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Статусы и настройки
|
|
||||||
is_active = models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
verbose_name="Активен",
|
|
||||||
help_text="Работает ли магазин в данный момент"
|
|
||||||
)
|
|
||||||
|
|
||||||
is_pickup_point = models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
verbose_name="Пункт самовывоза",
|
|
||||||
help_text="Доступен ли магазин для самовывоза заказов"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Временные метки
|
|
||||||
created_at = models.DateTimeField(
|
|
||||||
auto_now_add=True,
|
|
||||||
verbose_name="Дата создания"
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_at = models.DateTimeField(
|
|
||||||
auto_now=True,
|
|
||||||
verbose_name="Дата обновления"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "Магазин"
|
|
||||||
verbose_name_plural = "Магазины"
|
|
||||||
indexes = [
|
|
||||||
models.Index(fields=['is_active']),
|
|
||||||
models.Index(fields=['is_pickup_point']),
|
|
||||||
]
|
|
||||||
ordering = ['name']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.street and self.building_number:
|
|
||||||
return f"{self.name} ({self.full_address})"
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_address(self):
|
|
||||||
"""Полный адрес магазина"""
|
|
||||||
if self.street and self.building_number:
|
|
||||||
return f"{self.street}, {self.building_number}"
|
|
||||||
return ""
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Удалить магазин{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-8 col-lg-6 mx-auto">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header bg-danger text-white">
|
|
||||||
<h4 class="mb-0">Подтверждение удаления</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="lead">Вы уверены, что хотите удалить магазин <strong>{{ object.name }}</strong>?</p>
|
|
||||||
|
|
||||||
{% if object.full_address %}
|
|
||||||
<p class="text-muted">Адрес: {{ object.full_address }}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="alert alert-warning mt-3">
|
|
||||||
<strong>Внимание!</strong> Это действие нельзя отменить.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="post" class="mt-4">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-danger">Да, удалить</button>
|
|
||||||
<a href="{% url 'shops:shop_list' %}" class="btn btn-secondary">Отмена</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Основная информация -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Основная информация</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.name.label_tag }}
|
|
||||||
{{ form.name }}
|
|
||||||
{% if form.name.errors %}
|
|
||||||
<div class="text-danger">{{ form.name.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.description.label_tag }}
|
|
||||||
{{ form.description }}
|
|
||||||
{% if form.description.errors %}
|
|
||||||
<div class="text-danger">{{ form.description.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Адрес -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Адрес</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.street.label_tag }}
|
|
||||||
{{ form.street }}
|
|
||||||
{% if form.street.errors %}
|
|
||||||
<div class="text-danger">{{ form.street.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.building_number.label_tag }}
|
|
||||||
{{ form.building_number }}
|
|
||||||
{% if form.building_number.errors %}
|
|
||||||
<div class="text-danger">{{ form.building_number.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Контактная информация -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Контактная информация</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.phone.label_tag }}
|
|
||||||
{{ form.phone }}
|
|
||||||
<div class="form-text">Введите телефон в любом формате, например: +375291234567</div>
|
|
||||||
{% if form.phone.errors %}
|
|
||||||
<div class="text-danger">{{ form.phone.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.email.label_tag }}
|
|
||||||
{{ form.email }}
|
|
||||||
{% if form.email.errors %}
|
|
||||||
<div class="text-danger">{{ form.email.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Настройки -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Настройки</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
{{ form.is_active }}
|
|
||||||
{{ form.is_active.label_tag }}
|
|
||||||
{% if form.is_active.errors %}
|
|
||||||
<div class="text-danger">{{ form.is_active.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
{{ form.is_pickup_point }}
|
|
||||||
{{ form.is_pickup_point.label_tag }}
|
|
||||||
{% if form.is_pickup_point.errors %}
|
|
||||||
<div class="text-danger">{{ form.is_pickup_point.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Действия формы -->
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{{ button_text }}
|
|
||||||
</button>
|
|
||||||
<a href="{% url 'shops:shop_list' %}" class="btn btn-secondary">Отмена</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Магазины{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1>Магазины</h1>
|
|
||||||
<a href="{% url 'shops:shop_create' %}" class="btn btn-primary">
|
|
||||||
Добавить магазин
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if shops %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Название</th>
|
|
||||||
<th>Адрес</th>
|
|
||||||
<th>Телефон</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Пункт самовывоза</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for shop in shops %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ shop.name }}</td>
|
|
||||||
<td>{{ shop.full_address|default:"—" }}</td>
|
|
||||||
<td>{{ shop.phone|default:"—" }}</td>
|
|
||||||
<td>{{ shop.email|default:"—" }}</td>
|
|
||||||
<td>
|
|
||||||
{% if shop.is_pickup_point %}
|
|
||||||
<span class="badge bg-success">Да</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">Нет</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if shop.is_active %}
|
|
||||||
<span class="badge bg-success">Активен</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">Неактивен</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<a href="{% url 'shops:shop_update' shop.pk %}" class="btn btn-outline-primary">
|
|
||||||
Редактировать
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'shops:shop_delete' shop.pk %}" class="btn btn-outline-danger">
|
|
||||||
Удалить
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if is_paginated %}
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1">Первая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">
|
|
||||||
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<p class="mb-0">Магазины не найдены. <a href="{% url 'shops:shop_create' %}">Создать первый магазин</a></p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = 'shops'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.ShopListView.as_view(), name='shop_list'),
|
|
||||||
path('create/', views.ShopCreateView.as_view(), name='shop_create'),
|
|
||||||
path('<int:pk>/update/', views.ShopUpdateView.as_view(), name='shop_update'),
|
|
||||||
path('<int:pk>/delete/', views.ShopDeleteView.as_view(), name='shop_delete'),
|
|
||||||
]
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from django.urls import reverse_lazy
|
|
||||||
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
|
||||||
from .models import Shop
|
|
||||||
from .forms import ShopForm
|
|
||||||
|
|
||||||
|
|
||||||
class ShopListView(ListView):
|
|
||||||
"""Список всех магазинов"""
|
|
||||||
model = Shop
|
|
||||||
template_name = 'shops/shop_list.html'
|
|
||||||
context_object_name = 'shops'
|
|
||||||
paginate_by = 20
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Показываем только активные магазины по умолчанию"""
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
return queryset.filter(is_active=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ShopCreateView(CreateView):
|
|
||||||
"""Создание нового магазина"""
|
|
||||||
model = Shop
|
|
||||||
form_class = ShopForm
|
|
||||||
template_name = 'shops/shop_form.html'
|
|
||||||
success_url = reverse_lazy('shops:shop_list')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['title'] = 'Создать магазин'
|
|
||||||
context['button_text'] = 'Создать'
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class ShopUpdateView(UpdateView):
|
|
||||||
"""Редактирование магазина"""
|
|
||||||
model = Shop
|
|
||||||
form_class = ShopForm
|
|
||||||
template_name = 'shops/shop_form.html'
|
|
||||||
success_url = reverse_lazy('shops:shop_list')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['title'] = f'Редактировать: {self.object.name}'
|
|
||||||
context['button_text'] = 'Сохранить'
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class ShopDeleteView(DeleteView):
|
|
||||||
"""Удаление магазина"""
|
|
||||||
model = Shop
|
|
||||||
template_name = 'shops/shop_confirm_delete.html'
|
|
||||||
success_url = reverse_lazy('shops:shop_list')
|
|
||||||
@@ -38,9 +38,6 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'customers' %}active{% endif %}" href="{% url 'customers:customer-list' %}">Клиенты</a>
|
<a class="nav-link {% if request.resolver_match.namespace == 'customers' %}active{% endif %}" href="{% url 'customers:customer-list' %}">Клиенты</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'shops' %}active{% endif %}" href="{% url 'shops:shop_list' %}">Магазины</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Касса</a>
|
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Касса</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-11-13 13:12
|
# Generated by Django 5.0.10 on 2025-11-14 20:45
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
Reference in New Issue
Block a user