Объединение списков товаров и комплектов в единый интерфейс

- Создан единый шаблон products_list.html для отображения товаров и комплектов
- Удалены дублирующиеся шаблоны (product_list, productkit_list, products_unified_list, all_products_list)
- Добавлены фильтры: тип (все/товары/комплекты), категория, статус, наличие, теги
- Обновлен CombinedProductListView с поддержкой фильтрации по типу и тегам
- Изменены URL маршруты: главная страница /products/ теперь показывает объединенный список
- Обновлены success_url во всех CRUD представлениях для редиректа на объединенный список
- Добавлена фильтрация по тегам с отображением количества выбранных элементов
- Улучшена UX: компактный select для тегов с счетчиком выбранных
- Все комментарии в коде переведены на русский язык
This commit is contained in:
2025-11-15 22:48:34 +03:00
parent 9363527e50
commit b8185f2f6c
8 changed files with 409 additions and 768 deletions

View File

@@ -1,258 +0,0 @@
{% extends 'base.html' %}
{% block title %}Все товары и комплекты{% endblock %}
{% block content %}
<div class="container mt-5">
<h2 class="mb-4">Товары</h2>
<!-- Панель быстрых фильтров по категориям -->
{% include 'components/category_filter_buttons.html' with categories=filters.categories current_category=filters.current.category show_type_filters=True %}
<!-- Панель фильтрации и действий -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<!-- Кнопки действий -->
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<h5 class="card-title mb-0 me-3">
<i class="bi bi-funnel-fill"></i> Поиск и фильтры
</h5>
{% if action_buttons %}
<div class="btn-toolbar" role="toolbar">
{% for button in action_buttons %}
<a href="{{ button.url }}" class="btn {{ button.class|default:'btn-primary' }} btn-sm me-2 mb-2 mb-md-0">
{% if button.icon %}<i class="bi bi-{{ button.icon }}"></i>{% endif %}
{{ button.text }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
<hr class="my-3">
<!-- Форма фильтров -->
<form method="get" id="filterForm">
<div class="row g-3">
<!-- Поле поиска -->
<div class="col-12 col-md-6">
<label for="search" class="form-label">
<i class="bi bi-search"></i> Поиск
</label>
<input
type="text"
class="form-control"
id="search"
name="search"
placeholder="Поиск по названию, артикулу..."
value="{{ filters.current.search|default:'' }}"
>
</div>
<!-- Фильтр по статусу -->
<div class="col-12 col-md-3">
<label for="is_active" class="form-label">
<i class="bi bi-toggle-on"></i> Статус
</label>
<select class="form-select" id="is_active" name="is_active">
<option value="">Все</option>
<option value="1" {% if filters.current.is_active == '1' %}selected{% endif %}>Активные</option>
<option value="0" {% if filters.current.is_active == '0' %}selected{% endif %}>Неактивные</option>
</select>
</div>
<!-- Сохраняем текущую категорию при поиске -->
{% if filters.current.category %}
<input type="hidden" name="category" value="{{ filters.current.category }}">
{% endif %}
<!-- Кнопки управления фильтрами -->
<div class="col-12 col-md-3">
<label class="form-label d-none d-md-block">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-circle"></i> Применить
</button>
<a href="{{ request.path }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Сброс
</a>
</div>
</div>
</div>
</form>
</div>
</div>
{% if items %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th style="width: 60px;">Тип</th>
<th style="width: 80px;">Фото</th>
<th>Название</th>
<th>Артикул</th>
<th>Категория</th>
<th>Цена продажи</th>
<th>В наличии</th>
<th>Статус</th>
<th style="width: 200px;">Действия</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{% if item.item_type == 'product' %}
<span class="badge bg-success" title="Товар поштучно">
<i class="bi bi-box"></i>
</span>
{% else %}
<span class="badge bg-info" title="Комплект">
<i class="bi bi-box-seam"></i>
</span>
{% endif %}
</td>
<td>
{% if item.photos.all %}
{% with photo=item.photos.first %}
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
{% endwith %}
{% else %}
<span class="text-muted small">Нет фото</span>
{% endif %}
</td>
<td>
{% if item.item_type == 'product' %}
<a href="{% url 'products:product-detail' item.pk %}">{{ item.name }}</a>
{% else %}
<a href="{% url 'products:productkit-detail' item.pk %}">{{ item.name }}</a>
{% endif %}
</td>
<td>{{ item.sku }}</td>
<td>
{% if item.categories.all %}
{% for category in item.categories.all %}
<span class="badge bg-secondary">{{ category.name }}</span>{% if not forloop.last %} {% endif %}
{% endfor %}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if item.sale_price %}
<span class="text-decoration-line-through text-muted small">{{ item.price|floatformat:2 }} руб.</span>
<br>
<strong class="text-danger">{{ item.sale_price|floatformat:2 }} руб.</strong>
<span class="badge bg-danger ms-1">Акция</span>
{% else %}
<strong>{{ item.actual_price|floatformat:2 }} руб.</strong>
{% endif %}
</td>
<td>
{% if item.item_type == 'product' %}
{% if item.in_stock %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Да</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>
{% endif %}
{% else %}
<!-- Для комплектов проверяем есть ли вариант в наличии -->
<span class="text-muted small">-</span>
{% endif %}
</td>
<td>
{% if item.is_active %}
<span class="badge bg-info">Активен</span>
{% else %}
<span class="badge bg-secondary">Неактивен</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if item.item_type == 'product' %}
<a href="{% url 'products:product-detail' item.pk %}" class="btn btn-outline-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
{% if perms.products.change_product %}
<a href="{% url 'products:product-update' item.pk %}" class="btn btn-outline-primary" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
{% if perms.products.delete_product %}
<a href="{% url 'products:product-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
{% endif %}
{% else %}
<a href="{% url 'products:productkit-detail' item.pk %}" class="btn btn-outline-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
{% if perms.products.change_productkit %}
<a href="{% url 'products:productkit-update' item.pk %}" class="btn btn-outline-primary" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
{% if perms.products.delete_productkit %}
<a href="{% url 'products:productkit-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
{% endif %}
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% 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{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Предыдущая</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 }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<h4><i class="bi bi-info-circle"></i> Товары не найдены</h4>
<p>В данный момент нет товаров или комплектов, соответствующих выбранным фильтрам.</p>
<div class="mt-3">
{% if perms.products.add_product %}
<a href="{% url 'products:product-create' %}" class="btn btn-primary me-2">
<i class="bi bi-plus-circle"></i> Создать товар
</a>
{% endif %}
{% if perms.products.add_productkit %}
<a href="{% url 'products:productkit-create' %}" class="btn btn-outline-primary">
<i class="bi bi-plus-circle"></i> Создать комплект
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,136 +0,0 @@
{% extends 'base.html' %}
{% load quality_tags %}
{% block title %}Список товаров{% endblock %}
{% block content %}
<div class="container mt-5">
<h2 class="mb-4">Список товаров</h2>
<!-- Панель фильтрации -->
{% include 'components/filter_panel.html' with title="Товары" filters=filters action_buttons=action_buttons %}
{% if products %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Фото</th>
<th>Название</th>
<th>Артикул</th>
<th>Категория</th>
<th>Цена продажи</th>
<th>В наличии</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>
{% if product.photos.all %}
{% with photo=product.photos.first %}
<!-- Миниатюра с индикатором качества -->
<div class="photo-list-item">
<img src="{{ photo.get_thumbnail_url }}" alt="{{ product.name }}" class="img-thumbnail rounded">
<span class="quality-icon" title="{{ photo.get_quality_level_display }}">{{ photo|quality_icon_only }}</span>
</div>
{% endwith %}
{% else %}
<span class="text-muted">Нет фото</span>
{% endif %}
</td>
<td>
<a href="{% url 'products:product-detail' product.pk %}">{{ product.name }}</a>
</td>
<td>{{ product.sku }}</td>
<td>
{% if product.categories.all %}
{% for category in product.categories.all %}
{{ category.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
-
{% endif %}
</td>
<td>
{% if product.sale_price %}
<span class="text-decoration-line-through text-muted small">{{ product.price }} руб.</span>
<br>
<strong class="text-danger">{{ product.sale_price }} руб.</strong>
<span class="badge bg-danger ms-1">Акция</span>
{% else %}
<strong>{{ product.price }} руб.</strong>
{% endif %}
</td>
<td>
{% if product.in_stock %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> В наличии</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>
{% endif %}
</td>
<td>
{% if product.status == 'active' %}
<span class="badge bg-success">Активный</span>
{% elif product.status == 'archived' %}
<span class="badge bg-warning text-dark">Архивный</span>
{% elif product.status == 'discontinued' %}
<span class="badge bg-danger">Снят</span>
{% else %}
<span class="badge bg-secondary">{{ product.status }}</span>
{% endif %}
</td>
<td>
{% if perms.products.change_product %}
<a href="{% url 'products:product-update' product.pk %}" class="btn btn-sm btn-outline-primary">Изменить</a>
{% endif %}
{% if perms.products.delete_product %}
<a href="{% url 'products:product-delete' product.pk %}" class="btn btn-sm btn-outline-danger">Удалить</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% 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>Товары не найдены.</p>
{% if perms.products.add_product %}
<a href="{% url 'products:product-create' %}" class="btn btn-primary">Создать первый товар</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,141 +0,0 @@
{% extends 'base.html' %}
{% block title %}Список комплектов (букетов){% endblock %}
{% block content %}
<div class="container mt-5">
<h2 class="mb-4">Список комплектов (букетов)</h2>
<!-- Панель фильтрации -->
{% include 'components/filter_panel.html' with title="Комплекты" filters=filters action_buttons=action_buttons %}
{% if kits %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Фото</th>
<th>Название</th>
<th>Артикул</th>
<th>Категория</th>
<th>Цена продажи</th>
<th>Компонентов</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for kit in kits %}
<tr>
<td>
{% if kit.photos.all %}
{% with photo=kit.photos.first %}
<!-- Миниатюра 200x200 для списков -->
<img src="{{ photo.get_thumbnail_url }}" alt="{{ kit.name }}" style="width: 60px; height: 60px; object-fit: cover;" class="img-thumbnail rounded">
{% endwith %}
{% else %}
<span class="text-muted">Нет фото</span>
{% endif %}
</td>
<td>
<a href="{% url 'products:productkit-detail' kit.pk %}">{{ kit.name }}</a>
</td>
<td>{{ kit.sku }}</td>
<td>
{% if kit.categories.all %}
{% for category in kit.categories.all %}
{{ category.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
-
{% endif %}
</td>
<td>
{% if kit.sale_price %}
<span class="text-decoration-line-through text-muted small">{{ kit.price|floatformat:2 }} руб.</span>
<br>
<strong class="text-danger">{{ kit.sale_price|floatformat:2 }} руб.</strong>
<span class="badge bg-danger ms-1">Акция</span>
{% else %}
<strong>{{ kit.price|floatformat:2 }} руб.</strong>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ kit.get_total_components_count }} шт</span>
{% if kit.get_components_with_variants_count > 0 %}
<span class="badge bg-primary" title="С вариантами">
<i class="bi bi-shuffle"></i> {{ kit.get_components_with_variants_count }}
</span>
{% endif %}
</td>
<td>
{% if kit.status == 'active' %}
<span class="badge bg-success">Активный</span>
{% elif kit.status == 'archived' %}
<span class="badge bg-warning text-dark">Архивный</span>
{% elif kit.status == 'discontinued' %}
<span class="badge bg-danger">Снят</span>
{% else %}
<span class="badge bg-secondary">{{ kit.status }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'products:productkit-detail' kit.pk %}" class="btn btn-outline-info" title="Просмотреть">
<i class="bi bi-eye"></i>
</a>
<a href="{% url 'products:productkit-update' kit.pk %}" class="btn btn-outline-primary" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'products:productkit-delete' kit.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% 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{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Предыдущая</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 }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<h4><i class="bi bi-info-circle"></i> Комплекты не найдены</h4>
<p>В данный момент нет комплектов, соответствующих выбранным фильтрам.</p>
{% if perms.products.add_productkit %}
<a href="{% url 'products:productkit-create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Создать первый комплект
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,333 @@
{% extends 'base.html' %}
{% load quality_tags %}
{% block title %}Товары и комплекты{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<h2 class="mb-4">
<i class="bi bi-box-seam"></i> Товары и комплекты
</h2>
<!-- Панель фильтрации и действий -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<h5 class="card-title mb-0 me-3">
<i class="bi bi-funnel-fill"></i> Фильтры
</h5>
<div class="btn-toolbar" role="toolbar">
{% if action_buttons %}
{% for button in action_buttons %}
<a href="{{ button.url }}" class="btn {{ button.class|default:'btn-primary' }} btn-sm me-2 mb-2 mb-md-0">
{% if button.icon %}<i class="bi bi-{{ button.icon }}"></i>{% endif %}
{{ button.text }}
</a>
{% endfor %}
{% endif %}
</div>
</div>
<hr class="my-3">
<form method="get" id="filterForm">
<div class="row g-3">
<!-- Поиск -->
<div class="col-12 col-md-4">
<label for="search" class="form-label"><i class="bi bi-search"></i> Поиск</label>
<input type="text" class="form-control" id="search" name="search"
placeholder="Название, артикул, описание..."
value="{{ filters.current.search|default:'' }}">
</div>
<!-- Тип -->
<div class="col-12 col-md-2">
<label for="type" class="form-label"><i class="bi bi-box"></i> Тип</label>
<select class="form-select" id="type" name="type">
<option value="all" {% if filters.current.type == 'all' %}selected{% endif %}>Все</option>
<option value="products" {% if filters.current.type == 'products' %}selected{% endif %}>Только товары</option>
<option value="kits" {% if filters.current.type == 'kits' %}selected{% endif %}>Только комплекты</option>
</select>
</div>
<!-- Категория -->
<div class="col-12 col-md-3">
<label for="category" class="form-label"><i class="bi bi-bookmark"></i> Категория</label>
<select class="form-select" id="category" name="category">
<option value="">Все категории</option>
{% for category in filters.categories %}
<option value="{{ category.id }}" {% if filters.current.category == category.id|stringformat:"s" %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Статус -->
<div class="col-12 col-md-3">
<label for="status" class="form-label"><i class="bi bi-toggle-on"></i> Статус</label>
<select class="form-select" id="status" name="status">
<option value="">Все статусы</option>
{% for status_value, status_name in item_statuses %}
<option value="{{ status_value }}" {% if filters.current.status == status_value %}selected{% endif %}>
{{ status_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="row g-3 mt-2">
<!-- В наличии (только для товаров) -->
<div class="col-12 col-md-3">
<label for="in_stock" class="form-label"><i class="bi bi-check-circle"></i> В наличии</label>
<select class="form-select" id="in_stock" name="in_stock">
<option value="">Все</option>
<option value="1" {% if filters.current.in_stock == '1' %}selected{% endif %}>Да</option>
<option value="0" {% if filters.current.in_stock == '0' %}selected{% endif %}>Нет</option>
</select>
</div>
<!-- Теги -->
<div class="col-12 col-md-3">
<label for="tags" class="form-label"><i class="bi bi-tags"></i> Теги</label>
<select class="form-select" id="tags" name="tags" multiple size="1">
{% for tag in filters.tags %}
<option value="{{ tag.id }}" {% if tag.id in filters.current.tags %}selected{% endif %}>
{{ tag.name }}
</option>
{% endfor %}
</select>
{% if filters.current.tags %}
<small class="text-muted">Выбрано: {{ filters.current.tags|length }}</small>
{% endif %}
</div>
<div class="col-12 col-md-6">
<label class="form-label d-none d-md-block">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Применить
</button>
<a href="{% url 'products:products-list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Сбросить
</a>
</div>
</div>
</div>
</form>
</div>
</div>
{% if items %}
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th style="width: 80px;">Фото</th>
<th>Название</th>
<th style="width: 120px;">Артикул</th>
<th style="width: 80px;">Тип</th>
<th>Категория</th>
<th style="width: 130px;">Цена</th>
<th style="width: 100px;">В наличии</th>
<th style="width: 100px;">Статус</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{% if item.photos.all %}
{% with photo=item.photos.first %}
<div class="photo-list-item">
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}"
class="img-thumbnail rounded" style="width: 60px; height: 60px; object-fit: cover;">
{% if item.item_type == 'product' %}
<span class="quality-icon" title="{{ photo.get_quality_level_display }}">
{{ photo|quality_icon_only }}
</span>
{% endif %}
</div>
{% endwith %}
{% else %}
<span class="text-muted small">Нет фото</span>
{% endif %}
</td>
<td>
{% if item.item_type == 'product' %}
<a href="{% url 'products:product-detail' item.pk %}">{{ item.name }}</a>
{% else %}
<a href="{% url 'products:productkit-detail' item.pk %}">{{ item.name }}</a>
{% endif %}
</td>
<td><code class="small">{{ item.sku }}</code></td>
<td>
{% if item.item_type == 'product' %}
<span class="badge bg-success" title="Товар"><i class="bi bi-box"></i></span>
{% else %}
<span class="badge bg-info" title="Комплект"><i class="bi bi-box-seam"></i></span>
{% endif %}
</td>
<td>
{% for category in item.categories.all %}
<span class="badge bg-secondary">{{ category.name }}</span>
{% empty %}
<span class="text-muted">-</span>
{% endfor %}
</td>
<td>
{% if item.sale_price %}
<div class="text-decoration-line-through text-muted small">{{ item.price|floatformat:2 }} руб.</div>
<strong class="text-danger">{{ item.sale_price|floatformat:2 }} руб.</strong>
{% else %}
<strong>{{ item.price|floatformat:2 }} руб.</strong>
{% endif %}
</td>
<td>
{% if item.item_type == 'product' %}
{% if item.in_stock %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Да</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>
{% endif %}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if item.status == 'active' %}
<span class="badge bg-success">Активный</span>
{% elif item.status == 'archived' %}
<span class="badge bg-warning text-dark">Архивный</span>
{% elif item.status == 'discontinued' %}
<span class="badge bg-danger">Снят</span>
{% else %}
<span class="badge bg-secondary">{{ item.get_status_display }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if item.item_type == 'product' %}
<a href="{% url 'products:product-detail' item.pk %}"
class="btn btn-outline-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
{% if perms.products.change_product %}
<a href="{% url 'products:product-update' item.pk %}"
class="btn btn-outline-primary" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
{% if perms.products.delete_product %}
<a href="{% url 'products:product-delete' item.pk %}"
class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
{% endif %}
{% else %}
<a href="{% url 'products:productkit-detail' item.pk %}"
class="btn btn-outline-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
{% if perms.products.change_productkit %}
<a href="{% url 'products:productkit-update' item.pk %}"
class="btn btn-outline-primary" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
{% if perms.products.delete_productkit %}
<a href="{% url 'products:productkit-delete' item.pk %}"
class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
{% endif %}
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% 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{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
Первая
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
Предыдущая
</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 }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
Следующая
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
Последняя
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<h4><i class="bi bi-info-circle"></i> Товары не найдены</h4>
<p>В данный момент нет товаров или комплектов, соответствующих выбранным фильтрам.</p>
<div class="mt-3">
{% if perms.products.add_product %}
<a href="{% url 'products:product-create' %}" class="btn btn-primary me-2">
<i class="bi bi-plus-circle"></i> Создать товар
</a>
{% endif %}
{% if perms.products.add_productkit %}
<a href="{% url 'products:productkit-create' %}" class="btn btn-outline-primary">
<i class="bi bi-plus-circle"></i> Создать комплект
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<style>
/* Стили для индикатора качества фото */
.photo-list-item {
position: relative;
display: inline-block;
}
.photo-list-item .quality-icon {
position: absolute;
top: 2px;
right: 2px;
font-size: 14px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
</style>
{% endblock %}

View File

@@ -1,189 +0,0 @@
{% extends 'base.html' %}
{% load quality_tags %}
{% block title %}Список товаров и комплектов{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<h2 class="mb-4">Товары и Комплекты</h2>
<!-- Панель фильтрации и действий -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<h5 class="card-title mb-0 me-3">
<i class="bi bi-funnel-fill"></i> Фильтры
</h5>
<div class="btn-toolbar" role="toolbar">
{% if perms.products.add_product %}
<a href="{% url 'products:product-create' %}" class="btn btn-primary btn-sm me-2 mb-2 mb-md-0">
<i class="bi bi-plus-circle"></i> Создать товар
</a>
{% endif %}
{% if perms.products.add_productkit %}
<a href="{% url 'products:productkit-create' %}" class="btn btn-outline-primary btn-sm mb-2 mb-md-0">
<i class="bi bi-box-seam"></i> Создать комплект
</a>
{% endif %}
</div>
</div>
<hr class="my-3">
<form method="get" id="filterForm">
<div class="row g-3">
<!-- Поиск -->
<div class="col-12 col-md-4">
<label for="search" class="form-label"><i class="bi bi-search"></i> Поиск</label>
<input type="text" class="form-control" id="search" name="search" placeholder="Название, артикул, описание..." value="{{ filters.current.search|default:'' }}">
</div>
<!-- Тип -->
<div class="col-12 col-md-2">
<label for="type" class="form-label"><i class="bi bi-box-seam"></i> Тип</label>
<select class="form-select" id="type" name="type">
<option value="all" {% if filters.current.type == 'all' %}selected{% endif %}>Все</option>
<option value="products" {% if filters.current.type == 'products' %}selected{% endif %}>Только товары</option>
<option value="kits" {% if filters.current.type == 'kits' %}selected{% endif %}>Только комплекты</option>
</select>
</div>
<!-- Категория -->
<div class="col-12 col-md-3">
<label for="category" class="form-label"><i class="bi bi-bookmark"></i> Категория</label>
<select class="form-select" id="category" name="category">
<option value="">Все категории</option>
{% for category in filters.categories %}
<option value="{{ category.id }}" {% if filters.current.category == category.id|stringformat:"s" %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
<!-- Статус -->
<div class="col-12 col-md-3">
<label for="status" class="form-label"><i class="bi bi-toggle-on"></i> Статус</label>
<select class="form-select" id="status" name="status">
<option value="">Все статусы</option>
{% for status_value, status_name in item_statuses %}
<option value="{{ status_value }}" {% if filters.current.status == status_value %}selected{% endif %}>{{ status_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-12">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-circle"></i> Применить</button>
<a href="{% url 'products:product-list' %}" class="btn btn-outline-secondary"><i class="bi bi-x-circle"></i> Сброс</a>
</div>
</div>
</form>
</div>
</div>
{% if items %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Фото</th>
<th>Название</th>
<th>Артикул</th>
<th>Тип</th>
<th>Категория</th>
<th>Цена</th>
<th>В наличии</th>
<th>Компоненты</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{% with photo=item.photos.first %}
<div class="photo-list-item">
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}" class="img-thumbnail rounded">
{% if item.item_type == 'product' and photo %}
<span class="quality-icon" title="{{ photo.get_quality_level_display }}">{{ photo|quality_icon_only }}</span>
{% endif %}
</div>
{% endwith %}
</td>
<td>
<a href="{{ item.get_absolute_url }}">{{ item.name }}</a>
</td>
<td>{{ item.sku }}</td>
<td>
{% if item.item_type == 'product' %}
<span class="badge bg-success" title="Товар"><i class="bi bi-box"></i> Товар</span>
{% else %}
<span class="badge bg-info" title="Комплект"><i class="bi bi-box-seam"></i> Комплект</span>
{% endif %}
</td>
<td>
{% for category in item.categories.all %}
<span class="badge bg-secondary">{{ category.name }}</span>
{% empty %}
-
{% endfor %}
</td>
<td>
{% if item.sale_price %}
<span class="text-decoration-line-through text-muted small">{{ item.price|floatformat:2 }} руб.</span><br>
<strong class="text-danger">{{ item.sale_price|floatformat:2 }} руб.</strong>
{% else %}
<strong>{{ item.price|floatformat:2 }} руб.</strong>
{% endif %}
</td>
<td>
{% if item.item_type == 'product' %}
{% if item.in_stock %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Да</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>
{% endif %}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.item_type == 'kit' %}
<span class="badge bg-secondary">{{ item.get_total_components_count }} шт</span>
{% else %}
-
{% endif %}
</td>
<td>
<span class="badge" style="background-color: {{ item.get_status_color }}">{{ item.get_status_display }}</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ item.get_absolute_url }}" class="btn btn-outline-info" title="Просмотр"><i class="bi bi-eye"></i></a>
{% if item.item_type == 'product' and perms.products.change_product %}
<a href="{% url 'products:product-update' item.pk %}" class="btn btn-outline-primary" title="Изменить"><i class="bi bi-pencil"></i></a>
{% elif item.item_type == 'kit' and perms.products.change_productkit %}
<a href="{% url 'products:productkit-update' item.pk %}" class="btn btn-outline-primary" title="Изменить"><i class="bi bi-pencil"></i></a>
{% endif %}
{% if item.item_type == 'product' and perms.products.delete_product %}
<a href="{% url 'products:product-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить"><i class="bi bi-trash"></i></a>
{% elif item.item_type == 'kit' and perms.products.delete_productkit %}
<a href="{% url 'products:productkit-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить"><i class="bi bi-trash"></i></a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'components/pagination.html' %}
{% else %}
<div class="alert alert-info">
<p>Товары или комплекты не найдены.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -6,35 +6,37 @@ from .views import photo_status_api
app_name = 'products'
urlpatterns = [
# Combined view for products and kits
path('', views.CombinedProductListView.as_view(), name='all-products'),
# Main unified list for products and kits (default view)
path('', views.CombinedProductListView.as_view(), name='products-list'),
# Legacy URLs for backward compatibility
path('all/', views.CombinedProductListView.as_view(), name='all-products'),
path('products/', views.ProductListView.as_view(), name='product-list-legacy'),
path('kits/', views.ProductKitListView.as_view(), name='productkit-list'),
# CRUD URLs for Product
path('products/', views.ProductListView.as_view(), name='product-list'),
path('create/', views.ProductCreateView.as_view(), name='product-create'),
path('<int:pk>/', views.ProductDetailView.as_view(), name='product-detail'),
path('<int:pk>/update/', views.ProductUpdateView.as_view(), name='product-update'),
path('<int:pk>/delete/', views.ProductDeleteView.as_view(), name='product-delete'),
path('product/create/', views.ProductCreateView.as_view(), name='product-create'),
path('product/<int:pk>/', views.ProductDetailView.as_view(), name='product-detail'),
path('product/<int:pk>/update/', views.ProductUpdateView.as_view(), name='product-update'),
path('product/<int:pk>/delete/', views.ProductDeleteView.as_view(), name='product-delete'),
# Photo management
path('photo/<int:pk>/delete/', views.product_photo_delete, name='product-photo-delete'),
path('photo/<int:pk>/set-main/', views.product_photo_set_main, name='product-photo-set-main'),
path('photo/<int:pk>/move-up/', views.product_photo_move_up, name='product-photo-move-up'),
path('photo/<int:pk>/move-down/', views.product_photo_move_down, name='product-photo-move-down'),
# Photo management for Product
path('product/photo/<int:pk>/delete/', views.product_photo_delete, name='product-photo-delete'),
path('product/photo/<int:pk>/set-main/', views.product_photo_set_main, name='product-photo-set-main'),
path('product/photo/<int:pk>/move-up/', views.product_photo_move_up, name='product-photo-move-up'),
path('product/photo/<int:pk>/move-down/', views.product_photo_move_down, name='product-photo-move-down'),
# CRUD URLs for ProductKit (комплекты/букеты)
path('kits/', views.ProductKitListView.as_view(), name='productkit-list'),
path('kits/create/', views.ProductKitCreateView.as_view(), name='productkit-create'),
path('kits/<int:pk>/', views.ProductKitDetailView.as_view(), name='productkit-detail'),
path('kits/<int:pk>/update/', views.ProductKitUpdateView.as_view(), name='productkit-update'),
path('kits/<int:pk>/delete/', views.ProductKitDeleteView.as_view(), name='productkit-delete'),
path('kits/<int:pk>/make-permanent/', views.ProductKitMakePermanentView.as_view(), name='productkit-make-permanent'),
path('kit/create/', views.ProductKitCreateView.as_view(), name='productkit-create'),
path('kit/<int:pk>/', views.ProductKitDetailView.as_view(), name='productkit-detail'),
path('kit/<int:pk>/update/', views.ProductKitUpdateView.as_view(), name='productkit-update'),
path('kit/<int:pk>/delete/', views.ProductKitDeleteView.as_view(), name='productkit-delete'),
path('kit/<int:pk>/make-permanent/', views.ProductKitMakePermanentView.as_view(), name='productkit-make-permanent'),
# Photo management for ProductKit
path('kits/photo/<int:pk>/delete/', views.productkit_photo_delete, name='productkit-photo-delete'),
path('kits/photo/<int:pk>/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
path('kits/photo/<int:pk>/move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
path('kits/photo/<int:pk>/move-down/', views.productkit_photo_move_down, name='productkit-photo-move-down'),
path('kit/photo/<int:pk>/delete/', views.productkit_photo_delete, name='productkit-photo-delete'),
path('kit/photo/<int:pk>/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
path('kit/photo/<int:pk>/move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
path('kit/photo/<int:pk>/move-down/', views.productkit_photo_move_down, name='productkit-photo-move-down'),
# API endpoints
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),

View File

@@ -115,7 +115,7 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
permission_required = 'products.add_product'
def get_success_url(self):
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
def form_valid(self, form):
from django.db import IntegrityError
@@ -123,7 +123,7 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
try:
response = super().form_valid(form)
# Handle photo uploads
# Обработка загрузки фотографий
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
if photo_errors:
for error in photo_errors:
@@ -162,7 +162,7 @@ class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
permission_required = 'products.view_product'
def get_queryset(self):
# Prefetch photos to avoid N+1 queries
# Предзагрузка фотографий для избежания N+1 запросов
return super().get_queryset().prefetch_related('photos')
def get_context_data(self, **kwargs):
@@ -180,7 +180,7 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
permission_required = 'products.change_product'
def get_success_url(self):
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -195,7 +195,7 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
try:
response = super().form_valid(form)
# Handle photo uploads
# Обработка загрузки фотографий
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
if photo_errors:
for error in photo_errors:
@@ -235,29 +235,34 @@ class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView)
def get_success_url(self):
messages.success(self.request, f'Товар "{self.object.name}" успешно удален!')
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Объединенное представление для товаров и комплектов.
Показывает оба типа продуктов в одном списке.
Показывает оба типа продуктов в одном списке с возможностью фильтрации по типу.
"""
template_name = 'products/all_products_list.html'
template_name = 'products/products_list.html'
context_object_name = 'items'
permission_required = 'products.view_product'
paginate_by = 20
def get_queryset(self):
# Получаем фильтр по типу
type_filter = self.request.GET.get('type', 'all')
# Получаем товары и комплекты (только постоянные комплекты)
products = Product.objects.prefetch_related('categories', 'photos', 'tags')
kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos')
kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos', 'tags')
# Применяем фильтры
search_query = self.request.GET.get('search')
category_id = self.request.GET.get('category')
status_filter = self.request.GET.get('status')
is_active_filter = self.request.GET.get('is_active')
in_stock_filter = self.request.GET.get('in_stock')
tags = self.request.GET.getlist('tags')
# Фильтрация по поиску
if search_query:
@@ -293,15 +298,31 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
elif is_active_filter == '0':
products = products.filter(status__in=['archived', 'discontinued'])
kits = kits.filter(status__in=['archived', 'discontinued'])
# Фильтрация по наличию (только для товаров)
if in_stock_filter == '1':
products = products.filter(in_stock=True)
elif in_stock_filter == '0':
products = products.filter(in_stock=False)
# Фильтрация по тегам
if tags:
products = products.filter(tags__id__in=tags).distinct()
kits = kits.filter(tags__id__in=tags).distinct()
# Добавляем type для различения в шаблоне
products_list = list(products.order_by('-created_at'))
for p in products_list:
p.item_type = 'product'
kits_list = list(kits.order_by('-created_at'))
for k in kits_list:
k.item_type = 'kit'
# Применяем фильтр по типу
products_list = []
kits_list = []
if type_filter in ['all', 'products']:
products_list = list(products.order_by('-created_at'))
for p in products_list:
p.item_type = 'product'
if type_filter in ['all', 'kits']:
kits_list = list(kits.order_by('-created_at'))
for k in kits_list:
k.item_type = 'kit'
# Объединяем и сортируем по дате создания
combined = sorted(
@@ -315,16 +336,25 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Получаем список статусов из модели
from ..models.base import BaseProductEntity
item_statuses = BaseProductEntity.STATUS_CHOICES
# Данные для фильтров
context['filters'] = {
'categories': ProductCategory.objects.filter(is_active=True),
'tags': ProductTag.objects.all(),
'tags': ProductTag.objects.filter(is_active=True),
'current': {
'search': self.request.GET.get('search', ''),
'category': self.request.GET.get('category', ''),
'status': self.request.GET.get('status', ''),
'type': self.request.GET.get('type', 'all'),
'in_stock': self.request.GET.get('in_stock', ''),
'tags': [int(tag) for tag in self.request.GET.getlist('tags') if tag.isdigit()],
}
}
context['item_statuses'] = item_statuses
# Кнопки действий
action_buttons = []

View File

@@ -214,7 +214,7 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
f'Комплект "{self.object.name}" успешно создан!'
)
return redirect('products:productkit-list')
return redirect('products:products-list')
except IntegrityError as e:
# Обработка нарушения уникальности в БД
error_msg = str(e).lower()
@@ -416,7 +416,7 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
if self.request.POST.get('action') == 'continue':
return redirect('products:productkit-update', pk=self.object.pk)
else:
return redirect('products:productkit-list')
return redirect('products:products-list')
except IntegrityError as e:
# Обработка нарушения уникальности в БД
error_msg = str(e).lower()
@@ -450,7 +450,7 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
return self.render_to_response(context)
def get_success_url(self):
return reverse_lazy('products:productkit-list')
return reverse_lazy('products:products-list')
class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):