diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index ff98a96..314b148 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -5,7 +5,7 @@ from decimal import Decimal from .models import ( Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch, - TransferBatch, TransferItem + TransferBatch, TransferItem, Showcase ) from products.models import Product @@ -460,3 +460,4 @@ class TransferBulkForm(forms.Form): raise ValidationError('Склад-источник и склад-назначение должны быть разными') return cleaned_data + diff --git a/myproject/inventory/forms_showcase.py b/myproject/inventory/forms_showcase.py new file mode 100644 index 0000000..d1f0bcf --- /dev/null +++ b/myproject/inventory/forms_showcase.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from django import forms +from django.core.exceptions import ValidationError + +from .models import Showcase, Warehouse + + +class ShowcaseForm(forms.ModelForm): + """ + Форма для создания и редактирования витрин. + Витрина привязывается к складу и используется для выкладки готовых букетов. + """ + class Meta: + model = Showcase + fields = ['name', 'warehouse', 'description', 'is_active'] + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Название витрины (например: Витрина №1, Витрина у входа)' + }), + 'warehouse': forms.Select(attrs={'class': 'form-control'}), + 'description': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Описание витрины, её расположение или особенности' + }), + 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + } + labels = { + 'name': 'Название витрины', + 'warehouse': 'Склад', + 'description': 'Описание', + 'is_active': 'Активна', + } + help_texts = { + 'warehouse': 'Склад, к которому привязана витрина', + 'is_active': 'Неактивные витрины скрыты из списка выбора', + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Фильтруем только активные склады + self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True).order_by('name') + + # Если создаём новую витрину и есть склад по умолчанию - предвыбираем его + if not self.instance.pk and not self.initial.get('warehouse'): + default_warehouse = Warehouse.objects.filter( + is_active=True, + is_default=True + ).first() + if default_warehouse: + self.initial['warehouse'] = default_warehouse.id + + def clean_name(self): + """Проверка уникальности названия витрины в рамках склада""" + name = self.cleaned_data.get('name') + warehouse = self.cleaned_data.get('warehouse') + + if name and warehouse: + # Проверяем уникальность названия в рамках склада + queryset = Showcase.objects.filter(name=name, warehouse=warehouse) + + # При редактировании исключаем текущий экземпляр + if self.instance and self.instance.pk: + queryset = queryset.exclude(pk=self.instance.pk) + + if queryset.exists(): + raise ValidationError( + f'Витрина с названием "{name}" уже существует на складе "{warehouse.name}". ' + 'Пожалуйста, выберите другое название.' + ) + + return name diff --git a/myproject/inventory/templates/inventory/showcase/delete.html b/myproject/inventory/templates/inventory/showcase/delete.html new file mode 100644 index 0000000..6efb8d9 --- /dev/null +++ b/myproject/inventory/templates/inventory/showcase/delete.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+
+

+ Удаление витрины +

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

+ Вы действительно хотите удалить витрину "{{ object.name }}" + на складе "{{ object.warehouse.name }}"? +

+ + {% if has_active_reservations %} +
+
+ Невозможно удалить витрину +
+

+ На этой витрине есть {{ active_reservations.count }} активных резервов. + Сначала необходимо освободить или продать зарезервированные товары. +

+ + {% if active_reservations %} +
+

Активные резервы:

+
    + {% for reservation in active_reservations|slice:":5" %} +
  • {{ reservation.product.name }} — {{ reservation.quantity }} шт.
  • + {% endfor %} + {% if active_reservations.count > 5 %} +
  • ...и еще {{ active_reservations.count|add:"-5" }} резервов
  • + {% endif %} +
+ {% endif %} +
+ + + {% else %} +
+ + Внимание: Это действие нельзя отменить. Все данные о витрине будут удалены безвозвратно. +
+ +
+ {% csrf_token %} +
+ + + Отмена + +
+
+ {% endif %} +
+
+
+ + +
+
+
+
Информация о витрине
+
+
+
+
Название:
+
{{ object.name }}
+ +
Склад:
+
{{ object.warehouse.name }}
+ +
Статус:
+
+ {% if object.is_active %} + Активна + {% else %} + Неактивна + {% endif %} +
+ +
Создана:
+
{{ object.created_at|date:"d.m.Y H:i" }}
+ +
Обновлена:
+
{{ object.updated_at|date:"d.m.Y H:i" }}
+ +
Активные резервы:
+
+ {% if has_active_reservations %} + {{ active_reservations.count }} + {% else %} + + {% endif %} +
+
+
+
+
+
+
+{% endblock %} diff --git a/myproject/inventory/templates/inventory/showcase/form.html b/myproject/inventory/templates/inventory/showcase/form.html new file mode 100644 index 0000000..2c9d8bd --- /dev/null +++ b/myproject/inventory/templates/inventory/showcase/form.html @@ -0,0 +1,151 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+
+

+ {{ form_title }} +

+ +
+
+ + +
+
+
+
+
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + + +
+ + {{ form.name }} + {% if form.name.errors %} +
+ {{ form.name.errors }} +
+ {% endif %} + {% if form.name.help_text %} + {{ form.name.help_text }} + {% endif %} +
+ + +
+ + {{ form.warehouse }} + {% if form.warehouse.errors %} +
+ {{ form.warehouse.errors }} +
+ {% endif %} + {% if form.warehouse.help_text %} + {{ form.warehouse.help_text }} + {% endif %} +
+ + +
+ + {{ form.description }} + {% if form.description.errors %} +
+ {{ form.description.errors }} +
+ {% endif %} + {% if form.description.help_text %} + {{ form.description.help_text }} + {% endif %} +
+ + +
+
+ {{ form.is_active }} + + {% if form.is_active.help_text %} +
{{ form.is_active.help_text }}
+ {% endif %} +
+ {% if form.is_active.errors %} +
+ {{ form.is_active.errors }} +
+ {% endif %} +
+ + +
+ + + Отмена + +
+
+
+
+
+ + + {% if object %} +
+
+
+
Информация о витрине
+
+
+
+
Создана:
+
{{ object.created_at|date:"d.m.Y H:i" }}
+ +
Обновлена:
+
{{ object.updated_at|date:"d.m.Y H:i" }}
+ + {% if active_reservations_count is not None %} +
Активные резервы:
+
+ {% if active_reservations_count > 0 %} + {{ active_reservations_count }} + {% else %} + + {% endif %} +
+ {% endif %} +
+
+
+
+ {% endif %} +
+
+{% endblock %} diff --git a/myproject/inventory/templates/inventory/showcase/list.html b/myproject/inventory/templates/inventory/showcase/list.html new file mode 100644 index 0000000..af93d84 --- /dev/null +++ b/myproject/inventory/templates/inventory/showcase/list.html @@ -0,0 +1,162 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+
+
+
+

+ Витрины +

+ +
+
+ + Создать витрину + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+ + + Сбросить + +
+
+
+
+
+
+ + +
+
+ {% if showcases %} +
+
+
+ + + + + + + + + + + + + {% regroup showcases by warehouse as showcases_by_warehouse %} + {% for warehouse_group in showcases_by_warehouse %} + + + + + + {% for showcase in warehouse_group.list %} + + + + + + + + + {% endfor %} + {% endfor %} + +
НазваниеСкладОписаниеАктивные резервыСтатусДействия
+ {{ warehouse_group.grouper.name }} +
+ {{ showcase.name }} + + {{ showcase.warehouse.name }} + + {% if showcase.description %} + {{ showcase.description|truncatewords:10 }} + {% else %} + + {% endif %} + + {% if showcase.active_reservations_count > 0 %} + {{ showcase.active_reservations_count }} + {% else %} + + {% endif %} + + {% if showcase.is_active %} + Активна + {% else %} + Неактивна + {% endif %} + + + + + + + +
+
+
+
+ {% else %} +
+
+ +

Витрин не найдено

+

Создайте первую витрину для выкладки букетов

+ + Создать витрину + +
+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/myproject/inventory/urls.py b/myproject/inventory/urls.py index 0b57ee2..750bf04 100644 --- a/myproject/inventory/urls.py +++ b/myproject/inventory/urls.py @@ -26,6 +26,8 @@ from .views import ( # StockMovement StockMovementListView, ) +# Showcase views +from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView from . import views app_name = 'inventory' @@ -95,4 +97,10 @@ urlpatterns = [ # ==================== MOVEMENT (READ ONLY) ==================== path('movements/', StockMovementListView.as_view(), name='movement-list'), + + # ==================== SHOWCASE ==================== + path('showcases/', ShowcaseListView.as_view(), name='showcase-list'), + path('showcases/create/', ShowcaseCreateView.as_view(), name='showcase-create'), + path('showcases//edit/', ShowcaseUpdateView.as_view(), name='showcase-update'), + path('showcases//delete/', ShowcaseDeleteView.as_view(), name='showcase-delete'), ] diff --git a/myproject/inventory/views/showcase.py b/myproject/inventory/views/showcase.py new file mode 100644 index 0000000..2d1da6a --- /dev/null +++ b/myproject/inventory/views/showcase.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.shortcuts import render, redirect, get_object_or_404 +from django.db.models import Count, Q +from django.views.generic import ListView, CreateView, UpdateView, DeleteView +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator + +from inventory.models import Showcase, Reservation +from inventory.forms_showcase import ShowcaseForm + + +@method_decorator(login_required, name='dispatch') +class ShowcaseListView(ListView): + """ + Список всех витрин с группировкой по складам. + Отображает информацию о количестве активных резервов на каждой витрине. + """ + model = Showcase + template_name = 'inventory/showcase/list.html' + context_object_name = 'showcases' + + def get_queryset(self): + """ + Получаем витрины с аннотацией количества активных резервов. + Сортируем по складу и названию. + """ + queryset = Showcase.objects.select_related('warehouse').annotate( + active_reservations_count=Count( + 'reservations', + filter=Q(reservations__status='reserved') + ) + ).order_by('warehouse__name', 'name') + + # Фильтрация по складу, если указан GET-параметр + warehouse_id = self.request.GET.get('warehouse') + if warehouse_id: + queryset = queryset.filter(warehouse_id=warehouse_id) + + # Фильтрация по статусу активности + is_active = self.request.GET.get('is_active') + if is_active == '1': + queryset = queryset.filter(is_active=True) + elif is_active == '0': + queryset = queryset.filter(is_active=False) + + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Витрины' + + # Добавляем информацию для фильтров + from inventory.models import Warehouse + context['warehouses'] = Warehouse.objects.filter(is_active=True).order_by('name') + + return context + + +@method_decorator(login_required, name='dispatch') +class ShowcaseCreateView(CreateView): + """ + Создание новой витрины. + """ + model = Showcase + form_class = ShowcaseForm + template_name = 'inventory/showcase/form.html' + success_url = reverse_lazy('inventory:showcase-list') + + def form_valid(self, form): + """Сохраняем витрину и показываем сообщение об успехе""" + response = super().form_valid(form) + messages.success( + self.request, + f'Витрина "{self.object.name}" успешно создана на складе "{self.object.warehouse.name}"' + ) + return response + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Создание витрины' + context['form_title'] = 'Новая витрина' + context['submit_text'] = 'Создать витрину' + return context + + +@method_decorator(login_required, name='dispatch') +class ShowcaseUpdateView(UpdateView): + """ + Редактирование существующей витрины. + """ + model = Showcase + form_class = ShowcaseForm + template_name = 'inventory/showcase/form.html' + success_url = reverse_lazy('inventory:showcase-list') + + def form_valid(self, form): + """Сохраняем изменения и показываем сообщение об успехе""" + response = super().form_valid(form) + messages.success( + self.request, + f'Витрина "{self.object.name}" успешно обновлена' + ) + return response + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = f'Редактирование витрины: {self.object.name}' + context['form_title'] = f'Редактирование: {self.object.name}' + context['submit_text'] = 'Сохранить изменения' + + # Добавляем информацию о резервах на витрине + context['active_reservations_count'] = Reservation.objects.filter( + showcase=self.object, + status='reserved' + ).count() + + return context + + +@method_decorator(login_required, name='dispatch') +class ShowcaseDeleteView(DeleteView): + """ + Удаление витрины с подтверждением. + Проверяет наличие активных резервов перед удалением. + """ + model = Showcase + template_name = 'inventory/showcase/delete.html' + success_url = reverse_lazy('inventory:showcase-list') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = f'Удаление витрины: {self.object.name}' + + # Проверяем наличие активных резервов + active_reservations = Reservation.objects.filter( + showcase=self.object, + status='reserved' + ).select_related('product') + + context['active_reservations'] = active_reservations + context['has_active_reservations'] = active_reservations.exists() + + return context + + def delete(self, request, *args, **kwargs): + """Проверяем наличие активных резервов перед удалением""" + showcase = self.get_object() + + # Проверка активных резервов + active_reservations_count = Reservation.objects.filter( + showcase=showcase, + status='reserved' + ).count() + + if active_reservations_count > 0: + messages.error( + request, + f'Невозможно удалить витрину "{showcase.name}": ' + f'на ней есть {active_reservations_count} активных резервов. ' + 'Сначала освободите или продайте зарезервированные товары.' + ) + return redirect('inventory:showcase-delete', pk=showcase.pk) + + # Удаляем витрину + showcase_name = showcase.name + response = super().delete(request, *args, **kwargs) + + messages.success( + request, + f'Витрина "{showcase_name}" успешно удалена' + ) + + return response diff --git a/myproject/templates/navbar.html b/myproject/templates/navbar.html index fe2f1ee..35eb179 100644 --- a/myproject/templates/navbar.html +++ b/myproject/templates/navbar.html @@ -45,7 +45,10 @@ Касса + {% endif %}