refactor: Двухэтапное создание комплектов товаров
Реализован современный двухэтапный подход к созданию комплектов: **Шаг 1: Создание базовой информации** (productkit_create.html) - Упрощённая форма только с базовой информацией - Название, описание, категории, теги - Фотографии с предпросмотром - Настройки ценообразования - Чистый современный UI с breadcrumbs - Прогрессивное раскрытие (collapsible секции) **Шаг 2: Добавление товаров** (productkit_edit.html) - Автоматический redirect после создания - Работа с товарами через существующий функционал - Улучшенный заголовок с указанием шага **Изменения в коде:** - ProductKitCreateView: упрощён, убраны formsets - ProductKitUpdateView: использует новый шаблон - productkit_form.html → productkit_edit.html - Новый шаблон productkit_create.html **Преимущества:** ✅ Простая и надёжная логика сохранения ✅ Нет проблем с disabled полями в formsets ✅ Понятный UX с чёткими шагами ✅ Современный дизайн с иконками Bootstrap ✅ Предпросмотр фото перед загрузкой 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
363
myproject/products/templates/products/productkit_create.html
Normal file
363
myproject/products/templates/products/productkit_create.html
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Создать комплект{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<!-- Breadcrumbs -->
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'products:productkit-list' %}">Комплекты</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Создать комплект</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<i class="bi bi-box-seam"></i> Создание нового комплекта
|
||||||
|
</h4>
|
||||||
|
<small class="text-white-50">Шаг 1 из 2: Базовая информация</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Информационное сообщение -->
|
||||||
|
<div class="alert alert-info mb-4">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<strong>Как это работает:</strong> Сначала создайте комплект с базовой информацией,
|
||||||
|
затем вы сможете добавить товары на следующем шаге.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- 1. ОСНОВНАЯ ИНФОРМАЦИЯ -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<i class="bi bi-pencil-square"></i> Основная информация
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<!-- Название -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.name.id_for_label }}" class="form-label fw-bold required">
|
||||||
|
{{ form.name.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="text-danger">{{ form.name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Описание -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
|
{{ form.description.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{% if form.description.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="text-danger">{{ form.description.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Категории -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ form.categories.label }}</label>
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
{{ form.categories }}
|
||||||
|
</div>
|
||||||
|
{% if form.categories.errors %}
|
||||||
|
<div class="text-danger">{{ form.categories.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Теги -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ form.tags.label }}</label>
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
{{ form.tags }}
|
||||||
|
</div>
|
||||||
|
{% if form.tags.errors %}
|
||||||
|
<div class="text-danger">{{ form.tags.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. ФОТОГРАФИИ -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<i class="bi bi-images"></i> Фотографии комплекта
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_photos" class="form-label">
|
||||||
|
Загрузить фото
|
||||||
|
<span id="photoCount" class="badge bg-secondary ms-2" style="display: none;">0 выбрано</span>
|
||||||
|
</label>
|
||||||
|
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Выберите фото для комплекта (можно выбрать несколько, до 10 штук)
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<!-- Контейнер для предпросмотра выбранных фото -->
|
||||||
|
<div id="photoPreviewContainer" class="mt-3" style="display: none;">
|
||||||
|
<h6 class="mb-2">Выбранные фото:</h6>
|
||||||
|
<div id="photoPreview" class="row g-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. ЦЕНООБРАЗОВАНИЕ -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<i class="bi bi-calculator"></i> Ценообразование
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<strong>Обратите внимание:</strong> Метод ценообразования можно настроить после добавления товаров в комплект.
|
||||||
|
По умолчанию цена будет рассчитываться как сумма цен входящих товаров.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Метод ценообразования -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.pricing_method.id_for_label }}" class="form-label">
|
||||||
|
{{ form.pricing_method.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.pricing_method }}
|
||||||
|
{% if form.pricing_method.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.pricing_method.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.pricing_method.errors %}
|
||||||
|
<div class="text-danger">{{ form.pricing_method.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Фиксированная цена (показывается при выборе 'fixed') -->
|
||||||
|
<div id="fixedPriceBlock" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.fixed_price.id_for_label }}" class="form-label">
|
||||||
|
{{ form.fixed_price.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.fixed_price }}
|
||||||
|
{% if form.fixed_price.errors %}
|
||||||
|
<div class="text-danger">{{ form.fixed_price.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Процент наценки -->
|
||||||
|
<div id="markupPercentBlock" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.markup_percent.id_for_label }}" class="form-label">
|
||||||
|
{{ form.markup_percent.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.markup_percent }}
|
||||||
|
{% if form.markup_percent.errors %}
|
||||||
|
<div class="text-danger">{{ form.markup_percent.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Фиксированная наценка -->
|
||||||
|
<div id="markupAmountBlock" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.markup_amount.id_for_label }}" class="form-label">
|
||||||
|
{{ form.markup_amount.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.markup_amount }}
|
||||||
|
{% if form.markup_amount.errors %}
|
||||||
|
<div class="text-danger">{{ form.markup_amount.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (Collapsible) -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="border-bottom pb-2 mb-3">
|
||||||
|
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#additionalSettings" role="button" aria-expanded="false">
|
||||||
|
<i class="bi bi-gear"></i> Дополнительные настройки
|
||||||
|
<i class="bi bi-chevron-down float-end"></i>
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="collapse" id="additionalSettings">
|
||||||
|
<!-- Артикул -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.sku.id_for_label }}" class="form-label">
|
||||||
|
{{ form.sku.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.sku }}
|
||||||
|
{% if form.sku.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.sku.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.sku.errors %}
|
||||||
|
<div class="text-danger">{{ form.sku.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Активен -->
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.is_active }}
|
||||||
|
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||||
|
{{ form.is_active.label }}
|
||||||
|
</label>
|
||||||
|
{% if form.is_active.errors %}
|
||||||
|
<div class="text-danger">{{ form.is_active.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. КНОПКИ ДЕЙСТВИЙ (Sticky footer) -->
|
||||||
|
<div class="d-flex justify-content-between mt-4 border-top pt-3">
|
||||||
|
<a href="{% url 'products:productkit-list' %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle"></i> Отмена
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i> Далее: Добавить товары
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Подсказка -->
|
||||||
|
<div class="alert alert-light mt-3">
|
||||||
|
<strong>💡 Совет:</strong> После создания базовой информации вы попадёте на страницу редактирования,
|
||||||
|
где сможете добавить товары в комплект через удобный поиск.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.required::after {
|
||||||
|
content: " *";
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// ========== УПРАВЛЕНИЕ ВИДИМОСТЬЮ ПОЛЕЙ ЦЕНООБРАЗОВАНИЯ ==========
|
||||||
|
const pricingMethodField = document.querySelector('[name="pricing_method"]');
|
||||||
|
const fixedPriceBlock = document.getElementById('fixedPriceBlock');
|
||||||
|
const markupPercentBlock = document.getElementById('markupPercentBlock');
|
||||||
|
const markupAmountBlock = document.getElementById('markupAmountBlock');
|
||||||
|
|
||||||
|
function updatePricingFieldsVisibility() {
|
||||||
|
if (!pricingMethodField) return;
|
||||||
|
|
||||||
|
const method = pricingMethodField.value;
|
||||||
|
|
||||||
|
// Скрываем все блоки
|
||||||
|
fixedPriceBlock.style.display = 'none';
|
||||||
|
markupPercentBlock.style.display = 'none';
|
||||||
|
markupAmountBlock.style.display = 'none';
|
||||||
|
|
||||||
|
// Показываем нужный блок
|
||||||
|
if (method === 'fixed') {
|
||||||
|
fixedPriceBlock.style.display = 'block';
|
||||||
|
} else if (method === 'from_cost_plus_percent') {
|
||||||
|
markupPercentBlock.style.display = 'block';
|
||||||
|
} else if (method === 'from_cost_plus_amount') {
|
||||||
|
markupAmountBlock.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
updatePricingFieldsVisibility();
|
||||||
|
|
||||||
|
// Обработчик изменения метода
|
||||||
|
if (pricingMethodField) {
|
||||||
|
pricingMethodField.addEventListener('change', updatePricingFieldsVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ПРЕДПРОСМОТР ЗАГРУЖАЕМЫХ ФОТО ==========
|
||||||
|
const photoInput = document.getElementById('id_photos');
|
||||||
|
const photoPreview = document.getElementById('photoPreview');
|
||||||
|
const photoPreviewContainer = document.getElementById('photoPreviewContainer');
|
||||||
|
const photoCount = document.getElementById('photoCount');
|
||||||
|
|
||||||
|
let selectedFiles = [];
|
||||||
|
|
||||||
|
if (photoInput) {
|
||||||
|
photoInput.addEventListener('change', function(e) {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
selectedFiles = files;
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
photoCount.style.display = 'inline';
|
||||||
|
photoCount.textContent = `${files.length} выбрано`;
|
||||||
|
photoPreviewContainer.style.display = 'block';
|
||||||
|
photoPreview.innerHTML = '';
|
||||||
|
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(event) {
|
||||||
|
const col = document.createElement('div');
|
||||||
|
col.className = 'col-6 col-md-4 col-lg-3';
|
||||||
|
|
||||||
|
col.innerHTML = `
|
||||||
|
<div class="card position-relative" data-file-index="${index}">
|
||||||
|
<img src="${event.target.result}" class="card-img-top" alt="Preview" style="height: 150px; object-fit: cover;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<small class="text-truncate d-block">${file.name}</small>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger w-100 mt-1 remove-photo-btn" data-file-index="${index}">
|
||||||
|
<i class="bi bi-trash"></i> Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
photoPreview.appendChild(col);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
photoCount.style.display = 'none';
|
||||||
|
photoPreviewContainer.style.display = 'none';
|
||||||
|
photoPreview.innerHTML = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик удаления отдельных фото
|
||||||
|
photoPreview.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('remove-photo-btn') || e.target.closest('.remove-photo-btn')) {
|
||||||
|
const btn = e.target.classList.contains('remove-photo-btn') ? e.target : e.target.closest('.remove-photo-btn');
|
||||||
|
const fileIndex = parseInt(btn.getAttribute('data-file-index'));
|
||||||
|
|
||||||
|
selectedFiles.splice(fileIndex, 1);
|
||||||
|
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
selectedFiles.forEach(file => {
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
});
|
||||||
|
photoInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
photoInput.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
1085
myproject/products/templates/products/productkit_edit.html
Normal file
1085
myproject/products/templates/products/productkit_edit.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -83,72 +83,47 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
View для создания нового комплекта с компонентами.
|
View для создания нового комплекта (только базовая информация).
|
||||||
|
После создания redirect на страницу редактирования для добавления товаров.
|
||||||
"""
|
"""
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
form_class = ProductKitForm
|
form_class = ProductKitForm
|
||||||
template_name = 'products/productkit_form.html'
|
template_name = 'products/productkit_create.html'
|
||||||
permission_required = 'products.add_productkit'
|
permission_required = 'products.add_productkit'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
if self.request.POST:
|
|
||||||
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, instance=self.object)
|
|
||||||
else:
|
|
||||||
context['kititem_formset'] = KitItemFormSetCreate(instance=self.object)
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
# Получаем формсет из POST
|
try:
|
||||||
kititem_formset = KitItemFormSetCreate(self.request.POST, instance=self.object)
|
with transaction.atomic():
|
||||||
|
# Сохраняем основную форму
|
||||||
|
self.object = form.save()
|
||||||
|
|
||||||
# Проверяем валидность формсета
|
# Обработка фотографий
|
||||||
if kititem_formset.is_valid():
|
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
# Сохраняем основную форму
|
|
||||||
self.object = form.save()
|
|
||||||
|
|
||||||
# Сохраняем компоненты
|
messages.success(
|
||||||
kititem_formset.instance = self.object
|
self.request,
|
||||||
kititem_formset.save()
|
f'Комплект "{self.object.name}" создан! Теперь добавьте товары в комплект.'
|
||||||
|
)
|
||||||
|
|
||||||
# Обработка фотографий
|
# Всегда redirect на страницу редактирования для добавления товаров
|
||||||
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
|
return redirect('products:productkit-update', pk=self.object.pk)
|
||||||
|
|
||||||
messages.success(self.request, f'Комплект "{self.object.name}" успешно создан!')
|
except Exception as e:
|
||||||
|
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||||
# Проверяем, какую кнопку нажали
|
|
||||||
if self.request.POST.get('action') == 'continue':
|
|
||||||
return redirect('products:productkit-update', pk=self.object.pk)
|
|
||||||
else:
|
|
||||||
return redirect('products:productkit-list')
|
|
||||||
except Exception as e:
|
|
||||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
|
||||||
return self.form_invalid(form)
|
|
||||||
else:
|
|
||||||
# Если формсет невалиден, показываем форму с ошибками
|
|
||||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.')
|
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
# Получаем формсет для отображения ошибок
|
|
||||||
context = self.get_context_data(form=form)
|
|
||||||
return self.render_to_response(context)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('products:productkit-list')
|
# Этот метод не используется, т.к. мы делаем redirect в form_valid
|
||||||
|
return reverse_lazy('products:productkit-update', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
View для редактирования существующего комплекта.
|
View для редактирования существующего комплекта и добавления товаров.
|
||||||
"""
|
"""
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
form_class = ProductKitForm
|
form_class = ProductKitForm
|
||||||
template_name = 'products/productkit_form.html'
|
template_name = 'products/productkit_edit.html'
|
||||||
permission_required = 'products.change_productkit'
|
permission_required = 'products.change_productkit'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
Reference in New Issue
Block a user