Упрощена модель 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:
2025-11-12 00:10:34 +03:00
parent 7858c780d1
commit a1f5733fde
11 changed files with 477 additions and 80 deletions

View File

@@ -21,6 +21,7 @@ urlpatterns = [
path('customers/', include('customers.urls')), # Управление клиентами
path('inventory/', include('inventory.urls')), # Управление складом
path('orders/', include('orders.urls')), # Управление заказами
path('shops/', include('shops.urls')), # Управление магазинами
]
# Serve media files during development

View File

@@ -11,7 +11,6 @@ class ShopAdmin(admin.ModelAdmin):
'name',
'full_address',
'phone',
'working_hours',
'is_active',
'is_pickup_point',
]
@@ -19,7 +18,6 @@ class ShopAdmin(admin.ModelAdmin):
list_filter = [
'is_active',
'is_pickup_point',
'district',
]
search_fields = [
@@ -35,21 +33,14 @@ class ShopAdmin(admin.ModelAdmin):
'fields': ('name', 'description')
}),
('Адрес', {
'fields': ('street', 'building_number', 'district')
'fields': ('street', 'building_number')
}),
('Контакты', {
'fields': ('phone', 'email')
}),
('Режим работы', {
'fields': ('opening_time', 'closing_time', 'working_days')
}),
('Настройки', {
'fields': ('is_active', 'is_pickup_point')
}),
('Дополнительно', {
'fields': ('delivery_instructions', 'latitude', 'longitude'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']

56
myproject/shops/forms.py Normal file
View 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'})

View File

@@ -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='Улица'),
),
]

View File

@@ -4,34 +4,39 @@ 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,
verbose_name="Номер здания"
)
district = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name="Район",
help_text="Район в Минске"
verbose_name="Номер здания"
)
# Контактная информация
phone = PhoneNumberField(
blank=True,
null=True,
verbose_name="Телефон",
help_text="Контактный телефон магазина"
)
@@ -42,24 +47,6 @@ class Shop(models.Model):
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(
default=True,
@@ -73,40 +60,6 @@ class Shop(models.Model):
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(
auto_now_add=True,
@@ -124,19 +77,17 @@ class Shop(models.Model):
indexes = [
models.Index(fields=['is_active']),
models.Index(fields=['is_pickup_point']),
models.Index(fields=['district']),
]
ordering = ['name']
def __str__(self):
return f"{self.name} ({self.full_address})"
if self.street and self.building_number:
return f"{self.name} ({self.full_address})"
return self.name
@property
def full_address(self):
"""Полный адрес магазина"""
return f"{self.street}, {self.building_number}"
@property
def working_hours(self):
"""Форматированный режим работы"""
return f"{self.working_days}: {self.opening_time.strftime('%H:%M')} - {self.closing_time.strftime('%H:%M')}"
if self.street and self.building_number:
return f"{self.street}, {self.building_number}"
return ""

View 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 %}

View 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 %}

View 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
View 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'),
]

View File

@@ -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')

View File

@@ -27,6 +27,9 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'customers:customer-list' %}">Клиенты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'shops:shop_list' %}">Магазины</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Касса</a>
</li>