Добавлена модель атрибутов для вариативных товаров (ConfigurableKitProductAttribute)
- Создана модель ConfigurableKitProductAttribute с полями name, option, position, visible - Добавлены формы и formsets для управления атрибутами родительского товара - Обновлены CRUD представления для работы с атрибутами (создание/редактирование) - Добавлен блок атрибутов в шаблоны создания/редактирования - Обновлена страница детального просмотра с отображением атрибутов товара - Добавлен JavaScript для динамического добавления форм атрибутов - Реализована валидация дубликатов атрибутов в formset - Атрибуты сохраняются в transaction.atomic() вместе с вариантами Теперь можно определять схему атрибутов для экспорта на WooCommerce без использования JSON или ID, только name и option.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Удаление вариативного товара{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'products:configurablekit-list' %}">Вариативные товары</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'products:configurablekit-detail' object.pk %}">{{ object.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Удаление</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-exclamation-triangle me-2"></i>Подтверждение удаления</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-3">
|
||||
Вы уверены, что хотите удалить вариативный товар <strong>{{ object.name }}</strong>?
|
||||
</p>
|
||||
|
||||
{% if object.options.count > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-circle me-2"></i>
|
||||
<strong>Внимание:</strong> У этого вариативного товара есть {{ object.options.count }} вариант(ов).
|
||||
При удалении связи с комплектами будут удалены.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i>Да, удалить
|
||||
</button>
|
||||
<a href="{% url 'products:configurablekit-detail' object.pk %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Отмена
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,198 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ configurable_kit.name }} - Детали{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% csrf_token %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'products:configurablekit-list' %}">Вариативные товары</a></li>
|
||||
<li class="breadcrumb-item active">{{ configurable_kit.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Заголовок -->
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">{{ configurable_kit.name }}</h4>
|
||||
<div>
|
||||
<a href="{% url 'products:configurablekit-update' configurable_kit.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-pencil me-1"></i>Редактировать
|
||||
</a>
|
||||
<a href="{% url 'products:configurablekit-delete' configurable_kit.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-trash me-1"></i>Удалить
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Основная информация -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 200px;">Название:</th>
|
||||
<td>{{ configurable_kit.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Артикул:</th>
|
||||
<td>{{ configurable_kit.sku|default:"—" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Статус:</th>
|
||||
<td>
|
||||
{% if configurable_kit.status == 'active' %}
|
||||
<span class="badge bg-success">Активный</span>
|
||||
{% elif configurable_kit.status == 'archived' %}
|
||||
<span class="badge bg-warning">Архивный</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Снятый</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Краткое описание:</th>
|
||||
<td>{{ configurable_kit.short_description|default:"—" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Описание:</th>
|
||||
<td>{{ configurable_kit.description|default:"—" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата создания:</th>
|
||||
<td>{{ configurable_kit.created_at|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата обновления:</th>
|
||||
<td>{{ configurable_kit.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Варианты (комплекты) -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Варианты (комплекты)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if configurable_kit.options.all %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Комплект</th>
|
||||
<th>Артикул</th>
|
||||
<th>Цена</th>
|
||||
<th>Атрибуты</th>
|
||||
<th style="width: 120px;">По умолчанию</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for option in configurable_kit.options.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'products:productkit-detail' option.kit.pk %}" class="text-decoration-none">
|
||||
{{ option.kit.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td><small class="text-muted">{{ option.kit.sku|default:"—" }}</small></td>
|
||||
<td><strong>{{ option.kit.actual_price }}</strong> руб.</td>
|
||||
<td><small class="text-muted">{{ option.attributes|default:"—" }}</small></td>
|
||||
<td class="text-center">
|
||||
{% if option.is_default %}
|
||||
<span class="badge bg-primary">Да</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Нет</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted text-center py-4">
|
||||
Нет вариантов. Перейдите в режим редактирования для добавления.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Атрибуты родительского товара -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Атрибуты товара</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if configurable_kit.parent_attributes.all %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Название атрибута</th>
|
||||
<th>Значение опции</th>
|
||||
<th>Порядок</th>
|
||||
<th>Видимый</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for attr in configurable_kit.parent_attributes.all %}
|
||||
<tr>
|
||||
<td><strong>{{ attr.name }}</strong></td>
|
||||
<td>{{ attr.option }}</td>
|
||||
<td><span class="badge bg-secondary">{{ attr.position }}</span></td>
|
||||
<td>
|
||||
{% if attr.visible %}
|
||||
<span class="badge bg-success">Да</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Нет</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted text-center py-4">
|
||||
Нет атрибутов. Перейдите в режим редактирования для добавления.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Боковая панель -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">Справка</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">
|
||||
Вариативный товар предназначен для экспорта на WooCommerce и подобные площадки как Variable Product.
|
||||
</p>
|
||||
<p class="small text-muted">
|
||||
Каждый вариант — это отдельный ProductKit с собственной ценой, артикулом и атрибутами.
|
||||
</p>
|
||||
<hr>
|
||||
<p class="small text-muted mb-1">
|
||||
<strong>Количество вариантов:</strong> {{ configurable_kit.options.count }}
|
||||
</p>
|
||||
<p class="small text-muted mb-0">
|
||||
<strong>Атрибутов товара:</strong> {{ configurable_kit.parent_attributes.count }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
494
myproject/products/templates/products/configurablekit_form.html
Normal file
494
myproject/products/templates/products/configurablekit_form.html
Normal file
@@ -0,0 +1,494 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if object %}Редактирование{% else %}Создание{% endif %} вариативного товара{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Скрываем чекбоксы DELETE в formset */
|
||||
input[name*="DELETE"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Стили для switch */
|
||||
.form-check-input.is-default-switch {
|
||||
cursor: pointer;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.default-switch-label {
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.5rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
/* Выравнивание switch по центру */
|
||||
.col-md-2 .form-check.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'products:configurablekit-list' %}">Вариативные товары</a></li>
|
||||
<li class="breadcrumb-item active">{% if object %}Редактирование{% else %}Создание{% endif %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<!-- Основная информация -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">Название <span class="text-danger">*</span></label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small">{{ form.name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sku.id_for_label }}" class="form-label">Артикул</label>
|
||||
{{ form.sku }}
|
||||
{% if form.sku.errors %}
|
||||
<div class="text-danger small">{{ form.sku.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.short_description.id_for_label }}" class="form-label">Краткое описание</label>
|
||||
{{ form.short_description }}
|
||||
{% if form.short_description.errors %}
|
||||
<div class="text-danger small">{{ form.short_description.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Полное описание</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div class="text-danger small">{{ form.description.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">Статус</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}
|
||||
<div class="text-danger small">{{ form.status.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Варианты (комплекты) -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Варианты (комплекты)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ option_formset.management_form }}
|
||||
|
||||
{% if option_formset.non_form_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ option_formset.non_form_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="optionFormsetContainer">
|
||||
{% for form in option_formset %}
|
||||
<div class="option-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
{{ form.id }}
|
||||
{% if form.instance.pk %}
|
||||
<input type="hidden" name="options-{{ forloop.counter0 }}-id" value="{{ form.instance.pk }}">
|
||||
{% endif %}
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">{{ form.kit.label }}</label>
|
||||
{{ form.kit }}
|
||||
{% if form.kit.errors %}
|
||||
<div class="text-danger small">{{ form.kit.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.attributes.label }}</label>
|
||||
{{ form.attributes }}
|
||||
{% if form.attributes.errors %}
|
||||
<div class="text-danger small">{{ form.attributes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">{{ form.is_default.label }}</label>
|
||||
<div class="form-check form-switch">
|
||||
{{ form.is_default }}
|
||||
<label class="form-check-label" for="{{ form.is_default.id_for_label }}">
|
||||
<span class="default-switch-label">{% if form.instance.is_default %}Да{% else %}Нет{% endif %}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% if form.is_default.errors %}
|
||||
<div class="text-danger small">{{ form.is_default.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% if option_formset.can_delete %}
|
||||
<label class="form-label small d-block"> </label>
|
||||
{{ form.DELETE }}
|
||||
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addOptionBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить вариант
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Атрибуты родительского товара -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Атрибуты товара (для WooCommerce)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted mb-3">
|
||||
Определите схему атрибутов для вариативного товара. Например: Цвет=Красный, Размер=M, Длина=60см.
|
||||
</p>
|
||||
|
||||
{{ attribute_formset.management_form }}
|
||||
|
||||
{% if attribute_formset.non_form_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ attribute_formset.non_form_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="attributeFormsetContainer">
|
||||
{% for form in attribute_formset %}
|
||||
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
{{ form.id }}
|
||||
{% if form.instance.pk %}
|
||||
<input type="hidden" name="attributes-{{ forloop.counter0 }}-id" value="{{ form.instance.pk }}">
|
||||
{% endif %}
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.name.label }}</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small">{{ form.name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.option.label }}</label>
|
||||
{{ form.option }}
|
||||
{% if form.option.errors %}
|
||||
<div class="text-danger small">{{ form.option.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">{{ form.position.label }}</label>
|
||||
{{ form.position }}
|
||||
{% if form.position.errors %}
|
||||
<div class="text-danger small">{{ form.position.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">{{ form.visible.label }}</label>
|
||||
<div class="form-check">
|
||||
{{ form.visible }}
|
||||
<label class="form-check-label" for="{{ form.visible.id_for_label }}">
|
||||
Показывать
|
||||
</label>
|
||||
</div>
|
||||
{% if form.visible.errors %}
|
||||
<div class="text-danger small">{{ form.visible.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{% if attribute_formset.can_delete %}
|
||||
<label class="form-label small d-block"> </label>
|
||||
{{ form.DELETE }}
|
||||
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addAttributeBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить атрибут
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle me-1"></i>Сохранить
|
||||
</button>
|
||||
<a href="{% if object %}{% url 'products:configurablekit-detail' object.pk %}{% else %}{% url 'products:configurablekit-list' %}{% endif %}"
|
||||
class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Отмена
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Боковая панель -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">Справка</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">
|
||||
Вариативный товар объединяет несколько комплектов как варианты для экспорта на WooCommerce и подобные площадки.
|
||||
</p>
|
||||
<p class="small text-muted">
|
||||
Каждый вариант — это отдельный ProductKit с собственной ценой, артикулом и атрибутами.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Добавление новых форм вариантов
|
||||
document.getElementById('addOptionBtn').addEventListener('click', function() {
|
||||
const container = document.getElementById('optionFormsetContainer');
|
||||
const totalForms = document.querySelector('[name="options-TOTAL_FORMS"]');
|
||||
const formIdx = parseInt(totalForms.value);
|
||||
|
||||
// Создаём новую форму HTML
|
||||
const newFormHtml = `
|
||||
<div class="option-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Комплект</label>
|
||||
<select name="options-${formIdx}-kit" id="id_options-${formIdx}-kit" class="form-select">
|
||||
<option value="">---------</option>
|
||||
{% for kit in option_formset.empty_form.fields.kit.queryset %}
|
||||
<option value="{{ kit.id }}">{{ kit.name }}{% if kit.sku %} ({{ kit.sku }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Атрибуты варианта</label>
|
||||
<input type="text" name="options-${formIdx}-attributes"
|
||||
id="id_options-${formIdx}-attributes"
|
||||
class="form-control"
|
||||
placeholder="Например: Количество:15;Длина:60см">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">По умолчанию</label>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="options-${formIdx}-is_default"
|
||||
id="id_options-${formIdx}-is_default"
|
||||
class="form-check-input is-default-switch" role="switch">
|
||||
<label class="form-check-label" for="id_options-${formIdx}-is_default">
|
||||
<span class="default-switch-label">Нет</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small d-block"> </label>
|
||||
<input type="checkbox" name="options-${formIdx}-DELETE"
|
||||
id="id_options-${formIdx}-DELETE"
|
||||
style="display:none;">
|
||||
<label for="id_options-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', newFormHtml);
|
||||
totalForms.value = formIdx + 1;
|
||||
|
||||
// Переинициализируем логику switch после добавления новой формы
|
||||
initDefaultSwitches();
|
||||
});
|
||||
|
||||
// Скрытие удаленных форм
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.type === 'checkbox' && e.target.name && e.target.name.includes('DELETE')) {
|
||||
const form = e.target.closest('.option-form');
|
||||
if (e.target.checked) {
|
||||
form.style.opacity = '0.5';
|
||||
form.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
form.style.opacity = '1';
|
||||
form.style.textDecoration = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Логика для switch "По умолчанию"
|
||||
function initDefaultSwitches() {
|
||||
const container = document.getElementById('optionFormsetContainer');
|
||||
|
||||
// Функция для обновления текста label
|
||||
function updateSwitchLabel(switchInput) {
|
||||
const label = switchInput.closest('.form-check').querySelector('.default-switch-label');
|
||||
if (label) {
|
||||
label.textContent = switchInput.checked ? 'Да' : 'Нет';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для проверки и установки единственного варианта по умолчанию
|
||||
function ensureSingleDefault() {
|
||||
const visibleSwitches = Array.from(container.querySelectorAll('.is-default-switch')).filter(sw => {
|
||||
const form = sw.closest('.option-form');
|
||||
const deleteCheckbox = form.querySelector('input[name*="DELETE"]');
|
||||
return !deleteCheckbox || !deleteCheckbox.checked;
|
||||
});
|
||||
|
||||
// Если только один вариант - включаем его автоматически
|
||||
if (visibleSwitches.length === 1) {
|
||||
visibleSwitches[0].checked = true;
|
||||
visibleSwitches[0].disabled = true;
|
||||
updateSwitchLabel(visibleSwitches[0]);
|
||||
} else {
|
||||
// Если вариантов несколько - убираем disabled
|
||||
visibleSwitches.forEach(sw => {
|
||||
sw.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем, есть ли хотя бы один включенный
|
||||
const hasChecked = visibleSwitches.some(sw => sw.checked);
|
||||
if (!hasChecked && visibleSwitches.length > 0) {
|
||||
// Если ни один не включен, включаем первый
|
||||
visibleSwitches[0].checked = true;
|
||||
updateSwitchLabel(visibleSwitches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик изменения switch
|
||||
container.addEventListener('change', function(e) {
|
||||
if (e.target.classList.contains('is-default-switch')) {
|
||||
if (e.target.checked) {
|
||||
// Выключаем все остальные
|
||||
const allSwitches = container.querySelectorAll('.is-default-switch');
|
||||
allSwitches.forEach(sw => {
|
||||
if (sw !== e.target) {
|
||||
sw.checked = false;
|
||||
updateSwitchLabel(sw);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateSwitchLabel(e.target);
|
||||
ensureSingleDefault();
|
||||
}
|
||||
|
||||
// При изменении DELETE тоже проверяем
|
||||
if (e.target.name && e.target.name.includes('DELETE')) {
|
||||
ensureSingleDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация при загрузке
|
||||
ensureSingleDefault();
|
||||
container.querySelectorAll('.is-default-switch').forEach(updateSwitchLabel);
|
||||
}
|
||||
|
||||
// Запускаем инициализацию
|
||||
initDefaultSwitches();
|
||||
|
||||
// === Добавление новых форм атрибутов ===
|
||||
document.getElementById('addAttributeBtn').addEventListener('click', function() {
|
||||
const container = document.getElementById('attributeFormsetContainer');
|
||||
const totalForms = document.querySelector('[name="attributes-TOTAL_FORMS"]');
|
||||
const formIdx = parseInt(totalForms.value);
|
||||
|
||||
// Создаём новую форму HTML
|
||||
const newFormHtml = `
|
||||
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Название атрибута</label>
|
||||
<input type="text" name="attributes-${formIdx}-name"
|
||||
id="id_attributes-${formIdx}-name"
|
||||
class="form-control"
|
||||
placeholder="Например: Цвет, Размер, Длина">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Значение опции</label>
|
||||
<input type="text" name="attributes-${formIdx}-option"
|
||||
id="id_attributes-${formIdx}-option"
|
||||
class="form-control"
|
||||
placeholder="Например: Красный, M, 60см">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Порядок</label>
|
||||
<input type="number" name="attributes-${formIdx}-position"
|
||||
id="id_attributes-${formIdx}-position"
|
||||
class="form-control"
|
||||
min="0" value="0">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">Видимый</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="attributes-${formIdx}-visible"
|
||||
id="id_attributes-${formIdx}-visible"
|
||||
class="form-check-input" checked>
|
||||
<label class="form-check-label" for="id_attributes-${formIdx}-visible">
|
||||
Показывать
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block"> </label>
|
||||
<input type="checkbox" name="attributes-${formIdx}-DELETE"
|
||||
id="id_attributes-${formIdx}-DELETE"
|
||||
style="display:none;">
|
||||
<label for="id_attributes-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', newFormHtml);
|
||||
totalForms.value = formIdx + 1;
|
||||
});
|
||||
|
||||
// Скрытие удаленных атрибутов
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.type === 'checkbox' && e.target.name && e.target.name.includes('attributes') && e.target.name.includes('DELETE')) {
|
||||
const form = e.target.closest('.attribute-form');
|
||||
if (form) {
|
||||
if (e.target.checked) {
|
||||
form.style.opacity = '0.5';
|
||||
form.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
form.style.opacity = '1';
|
||||
form.style.textDecoration = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
146
myproject/products/templates/products/configurablekit_list.html
Normal file
146
myproject/products/templates/products/configurablekit_list.html
Normal file
@@ -0,0 +1,146 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Вариативные товары{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||
<li class="breadcrumb-item active">Вариативные товары</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Заголовок и кнопки действий -->
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Вариативные товары</h4>
|
||||
<div>
|
||||
{% for button in action_buttons %}
|
||||
<a href="{{ button.url }}" class="btn {{ button.class }} btn-sm">
|
||||
<i class="bi bi-{{ button.icon }} me-1"></i>{{ button.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поиск и фильтры -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body p-3">
|
||||
<form method="get" class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<input type="text" name="search" class="form-control form-control-sm"
|
||||
placeholder="Поиск по названию, артикулу, описанию..."
|
||||
value="{{ filters.current.search }}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="active" {% if filters.current.status == 'active' %}selected{% endif %}>Активные</option>
|
||||
<option value="archived" {% if filters.current.status == 'archived' %}selected{% endif %}>Архивные</option>
|
||||
<option value="discontinued" {% if filters.current.status == 'discontinued' %}selected{% endif %}>Снятые</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-outline-primary btn-sm w-100">
|
||||
<i class="bi bi-search"></i> Поиск
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Таблица -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Артикул</th>
|
||||
<th style="width: 120px;">Статус</th>
|
||||
<th style="width: 100px;">Вариантов</th>
|
||||
<th style="width: 150px;">Дата создания</th>
|
||||
<th style="width: 180px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in configurable_kits %}
|
||||
<tr>
|
||||
<td class="fw-semibold">
|
||||
<a href="{% url 'products:configurablekit-detail' item.pk %}" class="text-decoration-none">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ item.sku|default:"-" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.status == 'active' %}
|
||||
<span class="badge bg-success">Активный</span>
|
||||
{% elif item.status == 'archived' %}
|
||||
<span class="badge bg-warning">Архивный</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Снятый</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-info">{{ item.options.count }}</span>
|
||||
</td>
|
||||
<td><small class="text-muted">{{ item.created_at|date:"d.m.Y H:i" }}</small></td>
|
||||
<td>
|
||||
<a href="{% url 'products:configurablekit-detail' item.pk %}"
|
||||
class="btn btn-sm btn-outline-primary" title="Просмотр">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'products:configurablekit-update' item.pk %}"
|
||||
class="btn btn-sm btn-outline-warning" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'products:configurablekit-delete' item.pk %}"
|
||||
class="btn btn-sm btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Нет вариативных товаров. <a href="{% url 'products:configurablekit-create' %}">Создать первый</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
{% if is_paginated %}
|
||||
<nav class="mt-4" aria-label="Pagination">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if filters.current.search %}&search={{ filters.current.search }}{% endif %}{% if filters.current.status %}&status={{ filters.current.status }}{% endif %}">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if filters.current.search %}&search={{ filters.current.search }}{% endif %}{% if filters.current.status %}&status={{ filters.current.status }}{% endif %}">Предыдущая</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 }}{% if filters.current.search %}&search={{ filters.current.search }}{% endif %}{% if filters.current.status %}&status={{ filters.current.status }}{% endif %}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if filters.current.search %}&search={{ filters.current.search }}{% endif %}{% if filters.current.status %}&status={{ filters.current.status }}{% endif %}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user