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:
@@ -83,7 +83,7 @@ class OrderAdmin(admin.ModelAdmin):
|
||||
'is_delivery',
|
||||
'customer_is_recipient',
|
||||
'delivery_address',
|
||||
'pickup_shop',
|
||||
'pickup_warehouse',
|
||||
'delivery_date',
|
||||
'delivery_time_start',
|
||||
'delivery_time_end',
|
||||
|
||||
@@ -3,7 +3,7 @@ from django import forms
|
||||
from django.forms import inlineformset_factory
|
||||
from .models import Order, OrderItem, Address, OrderStatus
|
||||
from customers.models import Customer
|
||||
from shops.models import Shop
|
||||
from inventory.models import Warehouse
|
||||
from products.models import Product, ProductKit
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class OrderForm(forms.ModelForm):
|
||||
'customer',
|
||||
'is_delivery',
|
||||
'delivery_address',
|
||||
'pickup_shop',
|
||||
'pickup_warehouse',
|
||||
'delivery_date',
|
||||
'delivery_time_start',
|
||||
'delivery_time_end',
|
||||
@@ -145,11 +145,11 @@ class OrderForm(forms.ModelForm):
|
||||
})
|
||||
self.fields['delivery_address'].required = False
|
||||
|
||||
self.fields['pickup_shop'].widget.attrs.update({
|
||||
self.fields['pickup_warehouse'].widget.attrs.update({
|
||||
'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
|
||||
|
||||
@@ -12,7 +12,7 @@ from decimal import Decimal
|
||||
|
||||
from orders.models import Order, OrderItem, Address
|
||||
from customers.models import Customer
|
||||
from shops.models import Shop
|
||||
from inventory.models import Warehouse
|
||||
from products.models import Product
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
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:
|
||||
self.stdout.write(self.style.ERROR('Нет ни адресов, ни магазинов! Создайте хотя бы что-то одно.'))
|
||||
if not addresses and not warehouses:
|
||||
self.stdout.write(self.style.ERROR('Нет ни адресов, ни складов для самовывоза! Создайте хотя бы что-то одно.'))
|
||||
return
|
||||
|
||||
# Статусы и их вероятности
|
||||
@@ -111,8 +111,8 @@ class Command(BaseCommand):
|
||||
# Для доставки выбираем случайный адрес (адреса теперь привязаны к заказам)
|
||||
order.delivery_address = random.choice(addresses)
|
||||
order.delivery_cost = Decimal(random.randint(200, 500))
|
||||
elif shops:
|
||||
order.pickup_shop = random.choice(shops)
|
||||
elif warehouses:
|
||||
order.pickup_warehouse = random.choice(warehouses)
|
||||
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 simple_history.models
|
||||
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0001_initial'),
|
||||
('shops', '0001_initial'),
|
||||
('inventory', '0001_initial'),
|
||||
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='Код статуса')),
|
||||
('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_positive_end', models.BooleanField(default=False, help_text='True если это финальный успешный статус (Выполнен)', verbose_name='Положительный конец')),
|
||||
('is_negative_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='Отрицательный исход сделки')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
|
||||
('color', models.CharField(blank=True, default='#808080', help_text='Например: #FF5733', max_length=7, verbose_name='Цвет (hex)')),
|
||||
('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='Адрес доставки')),
|
||||
('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='Изменен пользователем')),
|
||||
('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={
|
||||
'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='Клиент')),
|
||||
('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='Изменен пользователем')),
|
||||
('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={
|
||||
'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
|
||||
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 customers.models import Customer
|
||||
from products.models import Product, ProductKit
|
||||
from shops.models import Shop
|
||||
from inventory.models import Warehouse
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
||||
@@ -283,14 +283,14 @@ class Order(models.Model):
|
||||
help_text="Обязательно для курьерской доставки"
|
||||
)
|
||||
|
||||
# Пункт самовывоза (для самовывоза)
|
||||
pickup_shop = models.ForeignKey(
|
||||
Shop,
|
||||
# Склад для самовывоза
|
||||
pickup_warehouse = models.ForeignKey(
|
||||
Warehouse,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='pickup_orders',
|
||||
verbose_name="Точка самовывоза",
|
||||
verbose_name="Склад для самовывоза",
|
||||
help_text="Обязательно для самовывоза"
|
||||
)
|
||||
|
||||
@@ -511,10 +511,10 @@ class Order(models.Model):
|
||||
'delivery_address': 'Для доставки необходимо указать адрес доставки'
|
||||
})
|
||||
|
||||
# Проверка: для самовывоза обязателен пункт самовывоза
|
||||
if not self.is_delivery and not self.pickup_shop:
|
||||
# Проверка: для самовывоза обязателен склад
|
||||
if not self.is_delivery and not self.pickup_warehouse:
|
||||
raise ValidationError({
|
||||
'pickup_shop': 'Для самовывоза необходимо выбрать точку самовывоза'
|
||||
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
|
||||
})
|
||||
|
||||
# Проверка: время окончания должно быть позже времени начала
|
||||
@@ -618,9 +618,9 @@ class Order(models.Model):
|
||||
return f"Доставка по адресу: {self.delivery_address.full_address}"
|
||||
return "Доставка (адрес не указан)"
|
||||
else:
|
||||
if self.pickup_shop:
|
||||
return f"Самовывоз из: {self.pickup_shop.name}"
|
||||
return "Самовывоз (точка не указана)"
|
||||
if self.pickup_warehouse:
|
||||
return f"Самовывоз со склада: {self.pickup_warehouse.name}"
|
||||
return "Самовывоз (склад не указан)"
|
||||
|
||||
@property
|
||||
def delivery_time_window(self):
|
||||
|
||||
@@ -57,7 +57,7 @@ class DraftOrderService:
|
||||
modified_by=user,
|
||||
is_delivery=data.get('is_delivery', True),
|
||||
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_time_start=data.get('delivery_time_start'),
|
||||
delivery_time_end=data.get('delivery_time_end'),
|
||||
@@ -97,7 +97,7 @@ class DraftOrderService:
|
||||
# ForeignKey поля требуют специальной обработки
|
||||
fk_fields = {
|
||||
'customer': 'customers.Customer',
|
||||
'pickup_shop': 'shops.Shop',
|
||||
'pickup_warehouse': 'inventory.Warehouse',
|
||||
'status': 'orders.OrderStatus',
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ class DraftOrderService:
|
||||
drafts = Order.objects.filter(
|
||||
status__code='draft',
|
||||
modified_by=user
|
||||
).select_related('customer', 'delivery_address', 'pickup_shop')
|
||||
).select_related('customer', 'delivery_address', 'pickup_warehouse')
|
||||
|
||||
if customer:
|
||||
drafts = drafts.filter(customer=customer)
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
'input[type="checkbox"]',
|
||||
'input[type="radio"]',
|
||||
'select[name="delivery_address"]',
|
||||
'select[name="pickup_shop"]',
|
||||
'select[name="pickup_warehouse"]',
|
||||
// Поля адреса доставки
|
||||
'input[name="address_street"]',
|
||||
'input[name="address_building_number"]',
|
||||
@@ -364,9 +364,9 @@
|
||||
data.delivery_address = parseInt(deliveryAddressField.value);
|
||||
}
|
||||
|
||||
const pickupShopField = form.querySelector('select[name="pickup_shop"]');
|
||||
if (pickupShopField && pickupShopField.value) {
|
||||
data.pickup_shop = parseInt(pickupShopField.value);
|
||||
const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
|
||||
if (pickupWarehouseField && pickupWarehouseField.value) {
|
||||
data.pickup_warehouse = parseInt(pickupWarehouseField.value);
|
||||
}
|
||||
|
||||
// Поля адреса доставки (новая логика с прямым вводом)
|
||||
|
||||
@@ -236,9 +236,9 @@
|
||||
data.delivery_address = parseInt(deliveryAddressField.value);
|
||||
}
|
||||
|
||||
const pickupShopField = form.querySelector('select[name="pickup_shop"]');
|
||||
if (pickupShopField && pickupShopField.value) {
|
||||
data.pickup_shop = parseInt(pickupShopField.value);
|
||||
const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
|
||||
if (pickupWarehouseField && pickupWarehouseField.value) {
|
||||
data.pickup_warehouse = parseInt(pickupWarehouseField.value);
|
||||
}
|
||||
|
||||
// Новая логика выбора адреса
|
||||
|
||||
@@ -130,13 +130,13 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<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">
|
||||
{% if order.pickup_shop %}
|
||||
{{ order.pickup_shop.name }}<br>
|
||||
<small class="text-muted">{{ order.pickup_shop.address }}</small>
|
||||
{% if order.pickup_warehouse %}
|
||||
{{ order.pickup_warehouse.name }}<br>
|
||||
<small class="text-muted">{{ order.pickup_warehouse.full_address }}</small>
|
||||
{% else %}
|
||||
<span class="text-danger">Не указана</span>
|
||||
<span class="text-danger">Не указан</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -522,12 +522,12 @@
|
||||
<div class="row" id="pickup-fields" style="display: none;">
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
{{ form.pickup_shop }}
|
||||
{% if form.pickup_shop.errors %}
|
||||
<div class="text-danger">{{ form.pickup_shop.errors }}</div>
|
||||
{{ form.pickup_warehouse }}
|
||||
{% if form.pickup_warehouse.errors %}
|
||||
<div class="text-danger">{{ form.pickup_warehouse.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ def order_list(request):
|
||||
"""
|
||||
# Базовый queryset с оптимизацией запросов
|
||||
orders = Order.objects.select_related(
|
||||
'customer', 'delivery_address', 'pickup_shop'
|
||||
'customer', 'delivery_address', 'pickup_warehouse'
|
||||
).all()
|
||||
|
||||
# Применяем фильтры через django-filter
|
||||
@@ -48,7 +48,7 @@ def order_list(request):
|
||||
def order_detail(request, pk):
|
||||
"""Детальная информация о заказе"""
|
||||
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'),
|
||||
pk=pk
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user