diff --git a/myproject/products/templates/products/all_products_list.html b/myproject/products/templates/products/all_products_list.html
index 64c9ead..6feb73a 100644
--- a/myproject/products/templates/products/all_products_list.html
+++ b/myproject/products/templates/products/all_products_list.html
@@ -95,6 +95,7 @@
Артикул |
Категория |
Цена продажи |
+ В наличии |
Статус |
Действия |
@@ -146,9 +147,21 @@
{{ item.get_sale_price|floatformat:2 }} руб.
{% endif %}
+
+ {% if item.item_type == 'product' %}
+ {% if item.in_stock %}
+ Да
+ {% else %}
+ Нет
+ {% endif %}
+ {% else %}
+
+ -
+ {% endif %}
+ |
{% if item.is_active %}
- Активен
+ Активен
{% else %}
Неактивен
{% endif %}
diff --git a/myproject/products/templates/products/product_detail.html b/myproject/products/templates/products/product_detail.html
index d4025d9..006ec5d 100644
--- a/myproject/products/templates/products/product_detail.html
+++ b/myproject/products/templates/products/product_detail.html
@@ -161,11 +161,21 @@
| Цена продажи: |
{{ product.sale_price }} руб. |
+
+ | В наличии: |
+
+ {% if product.in_stock %}
+ Да, в наличии
+ {% else %}
+ Нет, закончился
+ {% endif %}
+ |
+
| Статус: |
{% if product.is_active %}
- Активен
+ Активен
{% else %}
Неактивен
{% endif %}
diff --git a/myproject/products/templates/products/product_form.html b/myproject/products/templates/products/product_form.html
index 6c69bf1..7353130 100644
--- a/myproject/products/templates/products/product_form.html
+++ b/myproject/products/templates/products/product_form.html
@@ -138,6 +138,26 @@
+
+ {% if object %}
+
+ Информация о наличии
+
+ Статус:
+ {% if object.in_stock %}
+ В наличии
+ {% else %}
+ Нет в наличии
+ {% endif %}
+
+
+ Статус обновляется автоматически на основе остатков на складе
+
+
+ {% endif %}
+
+
+
Фотографии
diff --git a/myproject/products/templates/products/product_list.html b/myproject/products/templates/products/product_list.html
index 3261c98..bf8237a 100644
--- a/myproject/products/templates/products/product_list.html
+++ b/myproject/products/templates/products/product_list.html
@@ -19,6 +19,7 @@
| Артикул |
Категория |
Цена продажи |
+ В наличии |
Статус |
Действия |
@@ -50,9 +51,16 @@
{% endif %}
{{ product.sale_price }} руб. |
+
+ {% if product.in_stock %}
+ В наличии
+ {% else %}
+ Нет
+ {% endif %}
+ |
{% if product.is_active %}
- Активен
+ Активен
{% else %}
Неактивен
{% endif %}
diff --git a/myproject/products/templates/products/productkit_create.html b/myproject/products/templates/products/productkit_create.html
index bf23913..4e9ad26 100644
--- a/myproject/products/templates/products/productkit_create.html
+++ b/myproject/products/templates/products/productkit_create.html
@@ -326,75 +326,93 @@
animation: slideIn 0.3s ease-out;
}
+/* Разделитель ИЛИ между полями товара и вариантов */
+.kit-item-separator {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ min-height: 40px;
+}
+
+.kit-item-separator .separator-text {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #adb5bd;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ white-space: nowrap;
+}
+
+.kit-item-separator .separator-help {
+ font-size: 0.85rem;
+ color: #6c757d;
+ cursor: help;
+ transition: color 0.2s;
+}
+
+.kit-item-separator .separator-help:hover {
+ color: #0d6efd;
+}
+
+/* Bootstrap Tooltip стили */
+.tooltip-inner {
+ background-color: #2c3e50;
+ border-radius: 0.375rem;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.85rem;
+ max-width: 250px;
+ text-align: left;
+}
+
+.tooltip-arrow::before {
+ border-top-color: #2c3e50 !important;
+}
+
/* Адаптивность */
@media (max-width: 991px) {
.col-lg-8, .col-lg-4 {
max-width: 100%;
}
+
+ /* Для мобильных устройств делаем разделитель более компактным */
+ .kit-item-separator {
+ min-height: 35px;
+ }
+
+ .kit-item-separator .separator-text {
+ font-size: 0.7rem;
+ letter-spacing: 0.5px;
+ }
}
+
+{% include 'products/includes/select2-product-init.html' %}
+
+
+
+
+
+{% include 'products/includes/select2-product-init.html' %}
+
+
+{% endblock %}
diff --git a/myproject/products/templates/products/variantgroup_list.html b/myproject/products/templates/products/variantgroup_list.html
new file mode 100644
index 0000000..b56729a
--- /dev/null
+++ b/myproject/products/templates/products/variantgroup_list.html
@@ -0,0 +1,137 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block title %}Группы вариантов{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ Группы вариантов
+
+ Создать группу
+
+
+
+
+
+
+
+
+
+
+
+
+ | Название |
+ Описание |
+ Товаров |
+ В наличии |
+ Цена |
+ Дата обновления |
+ Действия |
+
+
+
+ {% for group in variant_groups %}
+
+ | {{ group.name }} |
+
+
+ {{ group.description|truncatewords:10 }}
+
+ |
+
+ {{ group.items_count }}
+ |
+
+ {% if group.in_stock %}
+ Да
+ {% else %}
+ Нет
+ {% endif %}
+ |
+
+ {% if group.price %}
+ {{ group.price }} руб.
+ {% else %}
+ -
+ {% endif %}
+ |
+ {{ group.updated_at|date:"d.m.Y H:i" }} |
+
+
+
+
+
+
+
+
+
+
+ |
+
+ {% empty %}
+
+ |
+ Нет групп вариантов. Создать первую
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+ {% if is_paginated %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/myproject/products/views/variant_group_views.py b/myproject/products/views/variant_group_views.py
new file mode 100644
index 0000000..405f228
--- /dev/null
+++ b/myproject/products/views/variant_group_views.py
@@ -0,0 +1,292 @@
+"""
+CRUD представления для групп вариантов товаров (ProductVariantGroup).
+Включает управление товарами в группе с приоритизацией.
+"""
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.decorators import login_required
+from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
+from django.urls import reverse_lazy, reverse
+from django.shortcuts import redirect, get_object_or_404
+from django.http import JsonResponse
+from django.db.models import Q, Count
+from django.db import transaction
+from django.views.decorators.http import require_http_methods
+
+from ..models import ProductVariantGroup, ProductVariantGroupItem
+from ..forms import (
+ ProductVariantGroupForm,
+ ProductVariantGroupItemFormSetCreate,
+ ProductVariantGroupItemFormSetUpdate
+)
+
+
+class ProductVariantGroupListView(LoginRequiredMixin, ListView):
+ """Список всех групп вариантов с поиском и фильтрацией"""
+ model = ProductVariantGroup
+ template_name = 'products/variantgroup_list.html'
+ context_object_name = 'variant_groups'
+ paginate_by = 20
+
+ def get_queryset(self):
+ queryset = super().get_queryset()
+
+ # Оптимизация: загружаем товары для вычисления in_stock и price
+ queryset = queryset.prefetch_related('items__product')
+
+ # Добавляем количество товаров в каждую группу
+ queryset = queryset.annotate(items_count=Count('items'))
+
+ # Поиск по названию и описанию
+ search_query = self.request.GET.get('search')
+ if search_query:
+ queryset = queryset.filter(
+ Q(name__icontains=search_query) |
+ Q(description__icontains=search_query)
+ )
+
+ return queryset.order_by('-updated_at')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['search_query'] = self.request.GET.get('search', '')
+ return context
+
+
+class ProductVariantGroupCreateView(LoginRequiredMixin, CreateView):
+ """Создание новой группы вариантов с добавлением товаров"""
+ model = ProductVariantGroup
+ form_class = ProductVariantGroupForm
+ template_name = 'products/variantgroup_form.html'
+ success_url = reverse_lazy('products:variantgroup-list')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ if self.request.POST:
+ context['items_formset'] = ProductVariantGroupItemFormSetCreate(
+ self.request.POST,
+ prefix='items'
+ )
+ else:
+ context['items_formset'] = ProductVariantGroupItemFormSetCreate(
+ prefix='items'
+ )
+ context['is_create'] = True
+ return context
+
+ def form_valid(self, form):
+ """Сохраняем группу и товары в одной транзакции"""
+ items_formset = ProductVariantGroupItemFormSetCreate(
+ self.request.POST,
+ prefix='items'
+ )
+
+ # Проверяем валидность основной формы и формсета
+ if not form.is_valid() or not items_formset.is_valid():
+ return self.form_invalid(form)
+
+ try:
+ with transaction.atomic():
+ # Сохраняем группу
+ self.object = form.save(commit=True)
+
+ # Сохраняем товары
+ items_formset.instance = self.object
+ items_formset.save()
+
+ # Пересчитываем приоритеты
+ self._recalculate_priorities(self.object)
+
+ messages.success(
+ self.request,
+ f'Группа вариантов "{self.object.name}" успешно создана!'
+ )
+ return redirect('products:variantgroup-list')
+ except Exception as e:
+ messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
+ return self.form_invalid(form)
+
+ @staticmethod
+ def _recalculate_priorities(variant_group):
+ """Пересчитывает приоритеты товаров в группе (по порядку в БД)"""
+ items = variant_group.items.all().order_by('id')
+ for idx, item in enumerate(items, start=1):
+ item.priority = idx
+ item.save(update_fields=['priority'])
+
+
+class ProductVariantGroupDetailView(LoginRequiredMixin, DetailView):
+ """Просмотр группы вариантов с список товаров в ней"""
+ model = ProductVariantGroup
+ template_name = 'products/variantgroup_detail.html'
+ context_object_name = 'variant_group'
+
+ def get_queryset(self):
+ queryset = super().get_queryset()
+ return queryset.prefetch_related('items__product')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ # Получаем товары с приоритетами
+ context['items'] = self.object.items.all().select_related('product').order_by('priority')
+ return context
+
+
+class ProductVariantGroupUpdateView(LoginRequiredMixin, UpdateView):
+ """Редактирование группы вариантов и управление товарами в ней"""
+ model = ProductVariantGroup
+ form_class = ProductVariantGroupForm
+ template_name = 'products/variantgroup_form.html'
+ success_url = reverse_lazy('products:variantgroup-list')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ if self.request.POST:
+ context['items_formset'] = ProductVariantGroupItemFormSetUpdate(
+ self.request.POST,
+ instance=self.object,
+ prefix='items'
+ )
+ else:
+ context['items_formset'] = ProductVariantGroupItemFormSetUpdate(
+ instance=self.object,
+ prefix='items'
+ )
+ context['is_create'] = False
+ return context
+
+ def form_valid(self, form):
+ """Сохраняем изменения группы и товаров"""
+ items_formset = ProductVariantGroupItemFormSetUpdate(
+ self.request.POST,
+ instance=self.object,
+ prefix='items'
+ )
+
+ if not form.is_valid() or not items_formset.is_valid():
+ return self.form_invalid(form)
+
+ try:
+ with transaction.atomic():
+ self.object = form.save(commit=True)
+ items_formset.instance = self.object
+ items_formset.save()
+
+ # Пересчитываем приоритеты после редактирования
+ self._recalculate_priorities(self.object)
+
+ messages.success(
+ self.request,
+ f'Группа вариантов "{self.object.name}" успешно обновлена!'
+ )
+ return redirect('products:variantgroup-list')
+ except Exception as e:
+ messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
+ return self.form_invalid(form)
+
+ @staticmethod
+ def _recalculate_priorities(variant_group):
+ """Пересчитывает приоритеты товаров в группе"""
+ items = variant_group.items.all().order_by('id')
+ for idx, item in enumerate(items, start=1):
+ item.priority = idx
+ item.save(update_fields=['priority'])
+
+
+class ProductVariantGroupDeleteView(LoginRequiredMixin, DeleteView):
+ """Удаление группы вариантов с подтверждением"""
+ model = ProductVariantGroup
+ template_name = 'products/variantgroup_confirm_delete.html'
+ success_url = reverse_lazy('products:variantgroup-list')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ # Добавляем информацию о товарах в группе
+ context['items'] = self.object.items.all().select_related('product').order_by('priority')
+ context['items_count'] = context['items'].count()
+ return context
+
+ def delete(self, request, *args, **kwargs):
+ """Удаляем группу и показываем сообщение об успехе"""
+ group_name = self.get_object().name
+ response = super().delete(request, *args, **kwargs)
+ messages.success(request, f'Группа вариантов "{group_name}" успешно удалена!')
+ return response
+
+
+@require_http_methods(["POST"])
+@login_required
+def product_variant_group_item_move(request, item_id, direction):
+ """
+ AJAX view для перемещения товара вверх/вниз в группе (UP/DOWN кнопки).
+ Перемещает товар вверх (up) или вниз (down) в списке приоритетов.
+ Автоматически пересчитывает приоритеты.
+ """
+ try:
+ # Получаем товар в группе
+ item = get_object_or_404(ProductVariantGroupItem, pk=item_id)
+ variant_group = item.variant_group
+
+ # Получаем все товары в группе отсортированные по приоритету
+ all_items = list(
+ variant_group.items.all()
+ .order_by('priority')
+ .values_list('id', 'priority')
+ )
+
+ # Находим текущую позицию товара
+ current_index = next(
+ (idx for idx, (item_pk, _) in enumerate(all_items) if item_pk == item_id),
+ None
+ )
+
+ if current_index is None:
+ return JsonResponse({'error': 'Товар не найден'}, status=404)
+
+ # Перемещаем товар
+ if direction == 'up' and current_index > 0:
+ # Меняем местами с товаром выше
+ all_items[current_index], all_items[current_index - 1] = \
+ all_items[current_index - 1], all_items[current_index]
+ elif direction == 'down' and current_index < len(all_items) - 1:
+ # Меняем местами с товаром ниже
+ all_items[current_index], all_items[current_index + 1] = \
+ all_items[current_index + 1], all_items[current_index]
+ else:
+ # Товар уже в крайней позиции
+ return JsonResponse({
+ 'error': f'Товар уже в крайней позиции',
+ 'items': _get_items_data(variant_group)
+ }, status=400)
+
+ # Пересчитываем приоритеты
+ with transaction.atomic():
+ for idx, (item_pk, _) in enumerate(all_items, start=1):
+ ProductVariantGroupItem.objects.filter(pk=item_pk).update(priority=idx)
+
+ # Возвращаем обновленный список товаров
+ return JsonResponse({
+ 'success': True,
+ 'message': f'Товар перемещен',
+ 'items': _get_items_data(variant_group)
+ })
+
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
+def _get_items_data(variant_group):
+ """Возвращает данные о товарах для обновления таблицы"""
+ items = variant_group.items.all().select_related('product').order_by('priority')
+ items_data = []
+ for item in items:
+ items_data.append({
+ 'id': item.id,
+ 'product_name': item.product.name,
+ 'product_sku': item.product.sku,
+ 'product_price': str(item.product.sale_price),
+ 'priority': item.priority,
+ 'can_move_up': item.priority > 1,
+ 'can_move_down': item.priority < items.count()
+ })
+ return items_data
|