Упрощена модель Shop и реализован полный CRUD для магазинов
- Упрощена модель Shop: только name обязательное поле - Удалены поля: district, режим работы, координаты, инструкции - Description перенесено после name - Все поля кроме name теперь опциональные - Создан полный CRUD для магазинов: * ShopListView - список магазинов с пагинацией * ShopCreateView - создание нового магазина * ShopUpdateView - редактирование магазина * ShopDeleteView - удаление с подтверждением - Создана форма ShopForm с Bootstrap стилями - Поле "Название магазина" помечено как обязательное (*) - Настроена обработка PhoneNumberField - Созданы шаблоны: * shop_list.html - таблица со списком магазинов * shop_form.html - форма создания/редактирования * shop_confirm_delete.html - подтверждение удаления - Настроены URLs для приложения shops - Добавлена ссылка "Магазины" в главную навигацию - Обновлена админ-панель shops 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ 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
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class ShopAdmin(admin.ModelAdmin):
|
|||||||
'name',
|
'name',
|
||||||
'full_address',
|
'full_address',
|
||||||
'phone',
|
'phone',
|
||||||
'working_hours',
|
|
||||||
'is_active',
|
'is_active',
|
||||||
'is_pickup_point',
|
'is_pickup_point',
|
||||||
]
|
]
|
||||||
@@ -19,7 +18,6 @@ class ShopAdmin(admin.ModelAdmin):
|
|||||||
list_filter = [
|
list_filter = [
|
||||||
'is_active',
|
'is_active',
|
||||||
'is_pickup_point',
|
'is_pickup_point',
|
||||||
'district',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
@@ -35,21 +33,14 @@ class ShopAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('name', 'description')
|
'fields': ('name', 'description')
|
||||||
}),
|
}),
|
||||||
('Адрес', {
|
('Адрес', {
|
||||||
'fields': ('street', 'building_number', 'district')
|
'fields': ('street', 'building_number')
|
||||||
}),
|
}),
|
||||||
('Контакты', {
|
('Контакты', {
|
||||||
'fields': ('phone', 'email')
|
'fields': ('phone', 'email')
|
||||||
}),
|
}),
|
||||||
('Режим работы', {
|
|
||||||
'fields': ('opening_time', 'closing_time', 'working_days')
|
|
||||||
}),
|
|
||||||
('Настройки', {
|
('Настройки', {
|
||||||
'fields': ('is_active', 'is_pickup_point')
|
'fields': ('is_active', 'is_pickup_point')
|
||||||
}),
|
}),
|
||||||
('Дополнительно', {
|
|
||||||
'fields': ('delivery_instructions', 'latitude', 'longitude'),
|
|
||||||
'classes': ('collapse',)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly_fields = ['created_at', 'updated_at']
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
|||||||
56
myproject/shops/forms.py
Normal file
56
myproject/shops/forms.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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'})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-11-11 20:55
|
||||||
|
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shops', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shop',
|
||||||
|
name='shops_shop_distric_04626c_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='closing_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='delivery_instructions',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='district',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='latitude',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='longitude',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='opening_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shop',
|
||||||
|
name='working_days',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='shop',
|
||||||
|
name='building_number',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='shop',
|
||||||
|
name='phone',
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Контактный телефон магазина', max_length=128, null=True, region=None, verbose_name='Телефон'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='shop',
|
||||||
|
name='street',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,34 +4,39 @@ from phonenumber_field.modelfields import PhoneNumberField
|
|||||||
|
|
||||||
class Shop(models.Model):
|
class Shop(models.Model):
|
||||||
"""
|
"""
|
||||||
Модель магазина/пункта самовывоза для цветочного магазина в Минске.
|
Модель магазина/пункта самовывоза для цветочного магазина.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
verbose_name="Название магазина"
|
verbose_name="Название магазина"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Описание",
|
||||||
|
help_text="Дополнительная информация о магазине"
|
||||||
|
)
|
||||||
|
|
||||||
# Адрес магазина
|
# Адрес магазина
|
||||||
street = models.CharField(
|
street = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name="Улица"
|
verbose_name="Улица"
|
||||||
)
|
)
|
||||||
|
|
||||||
building_number = models.CharField(
|
building_number = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
verbose_name="Номер здания"
|
|
||||||
)
|
|
||||||
|
|
||||||
district = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Район",
|
verbose_name="Номер здания"
|
||||||
help_text="Район в Минске"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Контактная информация
|
# Контактная информация
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name="Телефон",
|
verbose_name="Телефон",
|
||||||
help_text="Контактный телефон магазина"
|
help_text="Контактный телефон магазина"
|
||||||
)
|
)
|
||||||
@@ -42,24 +47,6 @@ class Shop(models.Model):
|
|||||||
verbose_name="Email"
|
verbose_name="Email"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Режим работы
|
|
||||||
opening_time = models.TimeField(
|
|
||||||
verbose_name="Время открытия",
|
|
||||||
help_text="Время начала работы магазина"
|
|
||||||
)
|
|
||||||
|
|
||||||
closing_time = models.TimeField(
|
|
||||||
verbose_name="Время закрытия",
|
|
||||||
help_text="Время окончания работы магазина"
|
|
||||||
)
|
|
||||||
|
|
||||||
working_days = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
default="Пн-Вс",
|
|
||||||
verbose_name="Рабочие дни",
|
|
||||||
help_text="Например: Пн-Пт, Пн-Вс, Пн-Сб"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Статусы и настройки
|
# Статусы и настройки
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
@@ -73,40 +60,6 @@ class Shop(models.Model):
|
|||||||
help_text="Доступен ли магазин для самовывоза заказов"
|
help_text="Доступен ли магазин для самовывоза заказов"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Дополнительная информация
|
|
||||||
description = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Описание",
|
|
||||||
help_text="Дополнительная информация о магазине"
|
|
||||||
)
|
|
||||||
|
|
||||||
delivery_instructions = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Инструкции для клиентов",
|
|
||||||
help_text="Как найти магазин, где припарковаться и т.д."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Координаты для карты (опционально)
|
|
||||||
latitude = models.DecimalField(
|
|
||||||
max_digits=9,
|
|
||||||
decimal_places=6,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name="Широта",
|
|
||||||
help_text="Координаты для отображения на карте"
|
|
||||||
)
|
|
||||||
|
|
||||||
longitude = models.DecimalField(
|
|
||||||
max_digits=9,
|
|
||||||
decimal_places=6,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name="Долгота",
|
|
||||||
help_text="Координаты для отображения на карте"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Временные метки
|
# Временные метки
|
||||||
created_at = models.DateTimeField(
|
created_at = models.DateTimeField(
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
@@ -124,19 +77,17 @@ class Shop(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['is_active']),
|
models.Index(fields=['is_active']),
|
||||||
models.Index(fields=['is_pickup_point']),
|
models.Index(fields=['is_pickup_point']),
|
||||||
models.Index(fields=['district']),
|
|
||||||
]
|
]
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.street and self.building_number:
|
||||||
return f"{self.name} ({self.full_address})"
|
return f"{self.name} ({self.full_address})"
|
||||||
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_address(self):
|
def full_address(self):
|
||||||
"""Полный адрес магазина"""
|
"""Полный адрес магазина"""
|
||||||
|
if self.street and self.building_number:
|
||||||
return f"{self.street}, {self.building_number}"
|
return f"{self.street}, {self.building_number}"
|
||||||
|
return ""
|
||||||
@property
|
|
||||||
def working_hours(self):
|
|
||||||
"""Форматированный режим работы"""
|
|
||||||
return f"{self.working_days}: {self.opening_time.strftime('%H:%M')} - {self.closing_time.strftime('%H:%M')}"
|
|
||||||
|
|||||||
36
myproject/shops/templates/shops/shop_confirm_delete.html
Normal file
36
myproject/shops/templates/shops/shop_confirm_delete.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% 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 %}
|
||||||
134
myproject/shops/templates/shops/shop_form.html
Normal file
134
myproject/shops/templates/shops/shop_form.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{% 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 %}
|
||||||
104
myproject/shops/templates/shops/shop_list.html
Normal file
104
myproject/shops/templates/shops/shop_list.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{% 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 %}
|
||||||
11
myproject/shops/urls.py
Normal file
11
myproject/shops/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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,3 +1,52 @@
|
|||||||
from django.shortcuts import render
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||||
|
from .models import Shop
|
||||||
|
from .forms import ShopForm
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
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')
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'customers:customer-list' %}">Клиенты</a>
|
<a class="nav-link" href="{% url 'customers:customer-list' %}">Клиенты</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user