diff --git a/myproject/myproject/urls.py b/myproject/myproject/urls.py index 6f9f273..73b0f29 100644 --- a/myproject/myproject/urls.py +++ b/myproject/myproject/urls.py @@ -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 diff --git a/myproject/shops/admin.py b/myproject/shops/admin.py index 19a8835..5c5f10e 100644 --- a/myproject/shops/admin.py +++ b/myproject/shops/admin.py @@ -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'] diff --git a/myproject/shops/forms.py b/myproject/shops/forms.py new file mode 100644 index 0000000..f0c6f01 --- /dev/null +++ b/myproject/shops/forms.py @@ -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'}) diff --git a/myproject/shops/migrations/0002_remove_shop_shops_shop_distric_04626c_idx_and_more.py b/myproject/shops/migrations/0002_remove_shop_shops_shop_distric_04626c_idx_and_more.py new file mode 100644 index 0000000..02cab45 --- /dev/null +++ b/myproject/shops/migrations/0002_remove_shop_shops_shop_distric_04626c_idx_and_more.py @@ -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='Улица'), + ), + ] diff --git a/myproject/shops/models.py b/myproject/shops/models.py index 1ccd8ed..5a34c84 100644 --- a/myproject/shops/models.py +++ b/myproject/shops/models.py @@ -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 "" diff --git a/myproject/shops/templates/shops/shop_confirm_delete.html b/myproject/shops/templates/shops/shop_confirm_delete.html new file mode 100644 index 0000000..9885510 --- /dev/null +++ b/myproject/shops/templates/shops/shop_confirm_delete.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}Удалить магазин{% endblock %} + +{% block content %} +
+
+
+
+
+

Подтверждение удаления

+
+
+

Вы уверены, что хотите удалить магазин {{ object.name }}?

+ + {% if object.full_address %} +

Адрес: {{ object.full_address }}

+ {% endif %} + +
+ Внимание! Это действие нельзя отменить. +
+ +
+ {% csrf_token %} +
+ + Отмена +
+
+
+
+
+
+
+{% endblock %} diff --git a/myproject/shops/templates/shops/shop_form.html b/myproject/shops/templates/shops/shop_form.html new file mode 100644 index 0000000..c2315b8 --- /dev/null +++ b/myproject/shops/templates/shops/shop_form.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+

{{ title }}

+ +
+ {% csrf_token %} + +
+ +
+
+
+
Основная информация
+
+
+
+ {{ form.name.label_tag }} + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+ +
+ {{ form.description.label_tag }} + {{ form.description }} + {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} +
+
+
+
+ + +
+
+
+
Адрес
+
+
+
+ {{ form.street.label_tag }} + {{ form.street }} + {% if form.street.errors %} +
{{ form.street.errors }}
+ {% endif %} +
+ +
+ {{ form.building_number.label_tag }} + {{ form.building_number }} + {% if form.building_number.errors %} +
{{ form.building_number.errors }}
+ {% endif %} +
+
+
+
+
+ +
+ +
+
+
+
Контактная информация
+
+
+
+ {{ form.phone.label_tag }} + {{ form.phone }} +
Введите телефон в любом формате, например: +375291234567
+ {% if form.phone.errors %} +
{{ form.phone.errors }}
+ {% endif %} +
+ +
+ {{ form.email.label_tag }} + {{ form.email }} + {% if form.email.errors %} +
{{ form.email.errors }}
+ {% endif %} +
+
+
+
+ + +
+
+
+
Настройки
+
+
+
+ {{ form.is_active }} + {{ form.is_active.label_tag }} + {% if form.is_active.errors %} +
{{ form.is_active.errors }}
+ {% endif %} +
+ +
+ {{ form.is_pickup_point }} + {{ form.is_pickup_point.label_tag }} + {% if form.is_pickup_point.errors %} +
{{ form.is_pickup_point.errors }}
+ {% endif %} +
+
+
+
+
+ + +
+ + Отмена +
+
+
+
+
+{% endblock %} diff --git a/myproject/shops/templates/shops/shop_list.html b/myproject/shops/templates/shops/shop_list.html new file mode 100644 index 0000000..baf255a --- /dev/null +++ b/myproject/shops/templates/shops/shop_list.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} + +{% block title %}Магазины{% endblock %} + +{% block content %} +
+
+
+ + + {% if shops %} +
+ + + + + + + + + + + + + + {% for shop in shops %} + + + + + + + + + + {% endfor %} + +
НазваниеАдресТелефонEmailПункт самовывозаСтатусДействия
{{ shop.name }}{{ shop.full_address|default:"—" }}{{ shop.phone|default:"—" }}{{ shop.email|default:"—" }} + {% if shop.is_pickup_point %} + Да + {% else %} + Нет + {% endif %} + + {% if shop.is_active %} + Активен + {% else %} + Неактивен + {% endif %} + + +
+
+ + {% if is_paginated %} + + {% endif %} + {% else %} +
+

Магазины не найдены. Создать первый магазин

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/myproject/shops/urls.py b/myproject/shops/urls.py new file mode 100644 index 0000000..fbc8594 --- /dev/null +++ b/myproject/shops/urls.py @@ -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('/update/', views.ShopUpdateView.as_view(), name='shop_update'), + path('/delete/', views.ShopDeleteView.as_view(), name='shop_delete'), +] diff --git a/myproject/shops/views.py b/myproject/shops/views.py index 91ea44a..3ea6280 100644 --- a/myproject/shops/views.py +++ b/myproject/shops/views.py @@ -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') diff --git a/myproject/templates/navbar.html b/myproject/templates/navbar.html index f9dcc17..b76cb82 100644 --- a/myproject/templates/navbar.html +++ b/myproject/templates/navbar.html @@ -27,6 +27,9 @@ +