feat: Add Product Kit creation and editing functionality with new views and templates.
This commit is contained in:
@@ -76,7 +76,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Загрузка с устройства -->
|
<!-- Загрузка с устройства -->
|
||||||
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm" id="id_photos">
|
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm"
|
||||||
|
id="id_photos">
|
||||||
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
||||||
<div id="photoPreview" class="row g-1"></div>
|
<div id="photoPreview" class="row g-1"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +98,8 @@
|
|||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<p class="small text-muted mb-3">
|
<p class="small text-muted mb-3">
|
||||||
Сгенерируйте привлекательное название для вашего букета автоматически
|
Сгенерируйте привлекательное название для вашего букета автоматически
|
||||||
<br><span class="badge bg-secondary mt-1">В базе: <span id="bouquetNamesCount">{{ bouquet_names_count }}</span> названий</span>
|
<br><span class="badge bg-secondary mt-1">В базе: <span id="bouquetNamesCount">{{
|
||||||
|
bouquet_names_count }}</span> названий</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex gap-2 mb-4">
|
<div class="d-flex gap-2 mb-4">
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" id="populateNamesBtn">
|
<button type="button" class="btn btn-outline-primary btn-sm" id="populateNamesBtn">
|
||||||
@@ -111,27 +113,36 @@
|
|||||||
<!-- Предложения названий -->
|
<!-- Предложения названий -->
|
||||||
<div class="name-suggestions">
|
<div class="name-suggestions">
|
||||||
<!-- Строка 1 -->
|
<!-- Строка 1 -->
|
||||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
|
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row"
|
||||||
|
data-name-id="">
|
||||||
<span class="text-muted small name-text">-</span>
|
<span class="text-muted small name-text">-</span>
|
||||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
<button type="button"
|
||||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Строка 2 -->
|
<!-- Строка 2 -->
|
||||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
|
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row"
|
||||||
|
data-name-id="">
|
||||||
<span class="text-muted small name-text">-</span>
|
<span class="text-muted small name-text">-</span>
|
||||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
<button type="button"
|
||||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Строка 3 -->
|
<!-- Строка 3 -->
|
||||||
<div class="d-flex justify-content-between align-items-center py-2 name-row" data-name-id="">
|
<div class="d-flex justify-content-between align-items-center py-2 name-row"
|
||||||
|
data-name-id="">
|
||||||
<span class="text-muted small name-text">-</span>
|
<span class="text-muted small name-text">-</span>
|
||||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
<button type="button"
|
||||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,8 +165,10 @@
|
|||||||
<!-- Базовая цена (отображение) -->
|
<!-- Базовая цена (отображение) -->
|
||||||
<div class="mb-3 p-2 rounded" style="background: #f8f9fa; border-left: 3px solid #6c757d;">
|
<div class="mb-3 p-2 rounded" style="background: #f8f9fa; border-left: 3px solid #6c757d;">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-muted small"><i class="bi bi-calculator"></i> Сумма цен компонентов:</span>
|
<span class="text-muted small"><i class="bi bi-calculator"></i> Сумма цен
|
||||||
<span id="basePriceDisplay" class="fw-semibold" style="font-size: 1.1rem;">0.00 руб.</span>
|
компонентов:</span>
|
||||||
|
<span id="basePriceDisplay" class="fw-semibold" style="font-size: 1.1rem;">0.00
|
||||||
|
руб.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,13 +182,15 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_increase_percent" class="form-control" placeholder="%" step="0.01" min="0">
|
<input type="number" id="id_increase_percent" class="form-control"
|
||||||
|
placeholder="%" step="0.01" min="0">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_increase_amount" class="form-control" placeholder="руб" step="0.01" min="0">
|
<input type="number" id="id_increase_amount" class="form-control"
|
||||||
|
placeholder="руб" step="0.01" min="0">
|
||||||
<span class="input-group-text">руб</span>
|
<span class="input-group-text">руб</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,13 +206,15 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_decrease_percent" class="form-control" placeholder="%" step="0.01" min="0">
|
<input type="number" id="id_decrease_percent" class="form-control"
|
||||||
|
placeholder="%" step="0.01" min="0">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_decrease_amount" class="form-control" placeholder="руб" step="0.01" min="0">
|
<input type="number" id="id_decrease_amount" class="form-control"
|
||||||
|
placeholder="руб" step="0.01" min="0">
|
||||||
<span class="input-group-text">руб</span>
|
<span class="input-group-text">руб</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,8 +227,10 @@
|
|||||||
<!-- Итоговая цена -->
|
<!-- Итоговая цена -->
|
||||||
<div class="p-2 rounded" style="background: #e7f5e7; border-left: 3px solid #198754;">
|
<div class="p-2 rounded" style="background: #e7f5e7; border-left: 3px solid #198754;">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-dark small"><i class="bi bi-check-circle me-1"></i><strong>Итоговая цена:</strong></span>
|
<span class="text-dark small"><i class="bi bi-check-circle me-1"></i><strong>Итоговая
|
||||||
<span id="finalPriceDisplay" class="fw-bold" style="font-size: 1.3rem; color: #198754;">0.00 руб.</span>
|
цена:</strong></span>
|
||||||
|
<span id="finalPriceDisplay" class="fw-bold"
|
||||||
|
style="font-size: 1.3rem; color: #198754;">0.00 руб.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,7 +246,8 @@
|
|||||||
<h6 class="mb-2 text-muted"><i class="bi bi-tag-discount"></i> Цена со скидкой</h6>
|
<h6 class="mb-2 text-muted"><i class="bi bi-tag-discount"></i> Цена со скидкой</h6>
|
||||||
<label class="form-label small mb-1">{{ form.sale_price.label }}</label>
|
<label class="form-label small mb-1">{{ form.sale_price.label }}</label>
|
||||||
{{ form.sale_price }}
|
{{ form.sale_price }}
|
||||||
<small class="form-text text-muted">Если указана, будет использоваться вместо расчетной цены</small>
|
<small class="form-text text-muted">Если указана, будет использоваться вместо расчетной
|
||||||
|
цены</small>
|
||||||
{% if form.sale_price.errors %}
|
{% if form.sale_price.errors %}
|
||||||
<div class="text-danger small mt-1">{{ form.sale_price.errors }}</div>
|
<div class="text-danger small mt-1">{{ form.sale_price.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -296,7 +316,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sticky Footer -->
|
<!-- Sticky Footer -->
|
||||||
<div class="sticky-bottom bg-white border-top mt-4 p-3 d-flex justify-content-between align-items-center shadow-sm">
|
<div
|
||||||
|
class="sticky-bottom bg-white border-top mt-4 p-3 d-flex justify-content-between align-items-center shadow-sm">
|
||||||
<a href="{% url 'products:products-list' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'products:products-list' %}" class="btn btn-outline-secondary">
|
||||||
Отмена
|
Отмена
|
||||||
</a>
|
</a>
|
||||||
@@ -308,14 +329,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Breadcrumbs */
|
/* Breadcrumbs */
|
||||||
.breadcrumb-sm {
|
.breadcrumb-sm {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Крупное поле названия */
|
/* Крупное поле названия */
|
||||||
#id_name {
|
#id_name {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: 3px solid #dee2e6;
|
border: 3px solid #dee2e6;
|
||||||
@@ -323,222 +344,229 @@
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_name:focus {
|
#id_name:focus {
|
||||||
border-color: #198754;
|
border-color: #198754;
|
||||||
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.15);
|
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.15);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Описание */
|
/* Описание */
|
||||||
#id_description {
|
#id_description {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Компактные чекбоксы */
|
/* Компактные чекбоксы */
|
||||||
.compact-checkboxes {
|
.compact-checkboxes {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes ul {
|
.compact-checkboxes ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes li {
|
.compact-checkboxes li {
|
||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes label {
|
.compact-checkboxes label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Компонент комплекта */
|
/* Компонент комплекта */
|
||||||
.kititem-form {
|
.kititem-form {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form:hover {
|
.kititem-form:hover {
|
||||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form .card-body {
|
.kititem-form .card-body {
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form input[type="checkbox"][name$="-DELETE"] {
|
.kititem-form input[type="checkbox"][name$="-DELETE"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sticky footer */
|
/* Sticky footer */
|
||||||
.sticky-bottom {
|
.sticky-bottom {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1020;
|
z-index: 1020;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Карточки */
|
/* Карточки */
|
||||||
.card.border-0 {
|
.card.border-0 {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Лейблы */
|
/* Лейблы */
|
||||||
.form-label.small {
|
.form-label.small {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Фото превью */
|
/* Фото превью */
|
||||||
#photoPreview .col-4,
|
#photoPreview .col-4,
|
||||||
#photoPreview .col-md-3,
|
#photoPreview .col-md-3,
|
||||||
#photoPreview .col-lg-2 {
|
#photoPreview .col-lg-2 {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoPreview .card {
|
#photoPreview .card {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoPreview img {
|
#photoPreview img {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert компактный */
|
/* Alert компактный */
|
||||||
.alert-sm {
|
.alert-sm {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Анимация */
|
/* Анимация */
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form.new-item {
|
.kititem-form.new-item {
|
||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Разделитель ИЛИ */
|
/* Разделитель ИЛИ */
|
||||||
.kit-item-separator {
|
.kit-item-separator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-text {
|
.kit-item-separator .separator-text {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-help {
|
.kit-item-separator .separator-help {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-help:hover {
|
.kit-item-separator .separator-help:hover {
|
||||||
color: #0d6efd;
|
color: #0d6efd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для генератора названий */
|
/* Стили для генератора названий */
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header[data-bs-toggle="collapse"]:hover {
|
.card-header[data-bs-toggle="collapse"]:hover {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header[data-bs-toggle="collapse"] .bi-chevron-down {
|
.card-header[data-bs-toggle="collapse"] .bi-chevron-down {
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse.show .card-header[data-bs-toggle="collapse"] .bi-chevron-down {
|
.collapse.show .card-header[data-bs-toggle="collapse"] .bi-chevron-down {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кнопки очень маленького размера */
|
/* Кнопки очень маленького размера */
|
||||||
.btn-xs {
|
.btn-xs {
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.125rem 0.25rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-xs:hover {
|
.btn-xs:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для списка предложений */
|
/* Стили для списка предложений */
|
||||||
.name-suggestions {
|
.name-suggestions {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-suggestions .text-muted {
|
.name-suggestions .text-muted {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-suggestions .border-bottom {
|
.name-suggestions .border-bottom {
|
||||||
border-color: #e9ecef !important;
|
border-color: #e9ecef !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для полей корректировки цены */
|
/* Стили для полей корректировки цены */
|
||||||
#id_increase_percent:disabled,
|
#id_increase_percent:disabled,
|
||||||
#id_increase_amount:disabled,
|
#id_increase_amount:disabled,
|
||||||
#id_decrease_percent:disabled,
|
#id_decrease_percent:disabled,
|
||||||
#id_decrease_amount:disabled {
|
#id_decrease_amount:disabled {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_increase_percent.is-invalid,
|
#id_increase_percent.is-invalid,
|
||||||
#id_increase_amount.is-invalid,
|
#id_increase_amount.is-invalid,
|
||||||
#id_decrease_percent.is-invalid,
|
#id_decrease_percent.is-invalid,
|
||||||
#id_decrease_amount.is-invalid {
|
#id_decrease_amount.is-invalid {
|
||||||
border-color: #dc3545;
|
border-color: #dc3545;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивность */
|
/* Адаптивность */
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
.col-lg-8, .col-lg-4 {
|
|
||||||
|
.col-lg-8,
|
||||||
|
.col-lg-4 {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Select2 инициализация -->
|
<!-- Select2 инициализация -->
|
||||||
{% include 'products/includes/select2-product-init.html' %}
|
{% include 'products/includes/select2-product-init.html' %}
|
||||||
|
|
||||||
|
{{ selected_products|default:"{}"|json_script:"selected-products-data" }}
|
||||||
|
{{ selected_variants|default:"{}"|json_script:"selected-variants-data" }}
|
||||||
|
{{ selected_sales_units|default:"{}"|json_script:"selected-sales-units-data" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// ========== УПРАВЛЕНИЕ ЦЕНОЙ КОМПЛЕКТА ==========
|
// ========== УПРАВЛЕНИЕ ЦЕНОЙ КОМПЛЕКТА ==========
|
||||||
const increasePercentInput = document.getElementById('id_increase_percent');
|
const increasePercentInput = document.getElementById('id_increase_percent');
|
||||||
const increaseAmountInput = document.getElementById('id_increase_amount');
|
const increaseAmountInput = document.getElementById('id_increase_amount');
|
||||||
@@ -550,6 +578,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const finalPriceDisplay = document.getElementById('finalPriceDisplay');
|
const finalPriceDisplay = document.getElementById('finalPriceDisplay');
|
||||||
|
|
||||||
let basePrice = 0;
|
let basePrice = 0;
|
||||||
|
let activeUpdates = 0; // Счетчик активных обновлений
|
||||||
|
|
||||||
// Кэш цен товаров для быстрого доступа
|
// Кэш цен товаров для быстрого доступа
|
||||||
const priceCache = {};
|
const priceCache = {};
|
||||||
@@ -743,6 +772,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Функция для обновления списка единиц продажи при выборе товара
|
// Функция для обновления списка единиц продажи при выборе товара
|
||||||
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
||||||
|
activeUpdates++; // Начинаем обновление
|
||||||
|
try {
|
||||||
|
// Сохраняем текущее значение перед очисткой (важно для редактирования и копирования)
|
||||||
|
let targetValue = salesUnitSelect.value;
|
||||||
|
|
||||||
|
// Если значения нет, проверяем preloaded данные (фаллбэк для инициализации)
|
||||||
|
if (!targetValue) {
|
||||||
|
const fieldName = salesUnitSelect.name;
|
||||||
|
if (selectedSalesUnits && selectedSalesUnits[fieldName]) {
|
||||||
|
targetValue = selectedSalesUnits[fieldName].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Очищаем текущие опции
|
// Очищаем текущие опции
|
||||||
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
salesUnitSelect.disabled = true;
|
salesUnitSelect.disabled = true;
|
||||||
@@ -761,7 +803,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(productId) || productId <= 0) {
|
if (isNaN(productId) || productId <= 0) {
|
||||||
console.warn('updateSalesUnitsOptions: invalid product id', productValue);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,17 +824,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
salesUnitSelect.appendChild(option);
|
salesUnitSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
salesUnitSelect.disabled = false;
|
salesUnitSelect.disabled = false;
|
||||||
// Обновляем Select2
|
|
||||||
|
// Восстанавливаем значение
|
||||||
|
if (targetValue) {
|
||||||
|
$(salesUnitSelect).val(targetValue).trigger('change');
|
||||||
|
} else {
|
||||||
|
// Обновляем Select2 без значения
|
||||||
$(salesUnitSelect).trigger('change');
|
$(salesUnitSelect).trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching sales units:', error);
|
console.error('Error fetching sales units:', error);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
activeUpdates--; // Завершаем обновление
|
||||||
|
if (activeUpdates === 0) {
|
||||||
|
calculateFinalPrice();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем data-product-id и загружаем цену при выборе товара
|
// Обновляем data-product-id и загружаем цену при выборе товара
|
||||||
$('[name$="-product"]').on('select2:select', async function() {
|
$('[name$="-product"]').on('select2:select', async function () {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
// Извлекаем числовой ID из "product_123"
|
// Извлекаем числовой ID из "product_123"
|
||||||
@@ -809,9 +862,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (salesUnitSelect) {
|
if (salesUnitSelect) {
|
||||||
await updateSalesUnitsOptions(salesUnitSelect, this.value);
|
await updateSalesUnitsOptions(salesUnitSelect, this.value);
|
||||||
}
|
}
|
||||||
calculateFinalPrice();
|
|
||||||
}
|
}
|
||||||
}).on('select2:unselect', function() {
|
calculateFinalPrice();
|
||||||
|
}).on('select2:unselect', function () {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
// Очищаем список единиц продажи
|
// Очищаем список единиц продажи
|
||||||
const salesUnitSelect = form.find('[name$="-sales_unit"]')[0];
|
const salesUnitSelect = form.find('[name$="-sales_unit"]')[0];
|
||||||
@@ -885,6 +938,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Функция для расчета финальной цены
|
// Функция для расчета финальной цены
|
||||||
async function calculateFinalPrice() {
|
async function calculateFinalPrice() {
|
||||||
|
// Если идут обновления - не считаем, ждем их завершения
|
||||||
|
if (activeUpdates > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем базовую цену (сумма всех компонентов)
|
// Получаем базовую цену (сумма всех компонентов)
|
||||||
let newBasePrice = 0;
|
let newBasePrice = 0;
|
||||||
const formsContainer = document.getElementById('kititem-forms');
|
const formsContainer = document.getElementById('kititem-forms');
|
||||||
@@ -1060,8 +1118,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Инициальный расчет (асинхронно)
|
// Инициальный расчет не нужен, так как он выполняется по событиям изменения полей
|
||||||
calculateFinalPrice();
|
// и после завершения загрузки единиц продажи
|
||||||
|
|
||||||
// ========== SELECT2 ИНИЦИАЛИЗАЦИЯ ==========
|
// ========== SELECT2 ИНИЦИАЛИЗАЦИЯ ==========
|
||||||
function initSelect2(element, type, preloadedData) {
|
function initSelect2(element, type, preloadedData) {
|
||||||
@@ -1072,23 +1130,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedProducts = {{ selected_products|default:"{}"|safe }};
|
const selectedProducts = JSON.parse(document.getElementById('selected-products-data').textContent || '{}');
|
||||||
const selectedVariants = {{ selected_variants|default:"{}"|safe }};
|
const selectedVariants = JSON.parse(document.getElementById('selected-variants-data').textContent || '{}');
|
||||||
const selectedSalesUnits = {{ selected_sales_units|default:"{}"|safe }};
|
const selectedSalesUnits = JSON.parse(document.getElementById('selected-sales-units-data').textContent || '{}');
|
||||||
|
|
||||||
$('[name$="-product"]').each(function() {
|
$('[name$="-product"]').each(function () {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
const preloadedData = selectedProducts[fieldName] || null;
|
const preloadedData = selectedProducts[fieldName] || null;
|
||||||
initSelect2(this, 'product', preloadedData);
|
initSelect2(this, 'product', preloadedData);
|
||||||
// Обработчик уже добавлен выше (строки 673-701)
|
// Обработчик уже добавлен выше (строки 673-701)
|
||||||
});
|
});
|
||||||
|
|
||||||
$('[name$="-variant_group"]').each(function() {
|
$('[name$="-variant_group"]').each(function () {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
const preloadedData = selectedVariants[fieldName] || null;
|
const preloadedData = selectedVariants[fieldName] || null;
|
||||||
initSelect2(this, 'variant', preloadedData);
|
initSelect2(this, 'variant', preloadedData);
|
||||||
// При выборе варианта очищаем единицу продажи
|
// При выборе варианта очищаем единицу продажи
|
||||||
$(this).on('select2:select', function() {
|
$(this).on('select2:select', function () {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
const salesUnitSelect = form.find('[name$="-sales_unit"]')[0];
|
const salesUnitSelect = form.find('[name$="-sales_unit"]')[0];
|
||||||
if (salesUnitSelect) {
|
if (salesUnitSelect) {
|
||||||
@@ -1099,7 +1157,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}).on('select2:unselect', calculateFinalPrice);
|
}).on('select2:unselect', calculateFinalPrice);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('[name$="-sales_unit"]').each(function() {
|
$('[name$="-sales_unit"]').each(function () {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
const preloadedData = selectedSalesUnits[fieldName] || null;
|
const preloadedData = selectedSalesUnits[fieldName] || null;
|
||||||
initSelect2(this, 'sales_unit', preloadedData);
|
initSelect2(this, 'sales_unit', preloadedData);
|
||||||
@@ -1169,7 +1227,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (quantityInput) {
|
if (quantityInput) {
|
||||||
quantityInput.addEventListener('change', calculateFinalPrice);
|
quantityInput.addEventListener('change', calculateFinalPrice);
|
||||||
// Выделяем весь текст при фокусе на поле количества
|
// Выделяем весь текст при фокусе на поле количества
|
||||||
quantityInput.addEventListener('focus', function() {
|
quantityInput.addEventListener('focus', function () {
|
||||||
this.select();
|
this.select();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1238,7 +1296,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
initSelect2(salesUnitSelect, 'sales_unit');
|
initSelect2(salesUnitSelect, 'sales_unit');
|
||||||
|
|
||||||
// Добавляем обработчики для новой формы (как в основном коде)
|
// Добавляем обработчики для новой формы (как в основном коде)
|
||||||
$(productSelect).on('select2:select', async function() {
|
$(productSelect).on('select2:select', async function () {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
let numericId = this.value;
|
let numericId = this.value;
|
||||||
@@ -1252,7 +1310,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
}).on('select2:unselect', function() {
|
}).on('select2:unselect', function () {
|
||||||
if (salesUnitSelect) {
|
if (salesUnitSelect) {
|
||||||
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
$(salesUnitSelect).trigger('change');
|
$(salesUnitSelect).trigger('change');
|
||||||
@@ -1260,7 +1318,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(variantSelect).on('select2:select', function() {
|
$(variantSelect).on('select2:select', function () {
|
||||||
if (salesUnitSelect) {
|
if (salesUnitSelect) {
|
||||||
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
$(salesUnitSelect).trigger('change');
|
$(salesUnitSelect).trigger('change');
|
||||||
@@ -1290,7 +1348,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
|
||||||
if (photoInput) {
|
if (photoInput) {
|
||||||
photoInput.addEventListener('change', function(e) {
|
photoInput.addEventListener('change', function (e) {
|
||||||
selectedFiles = Array.from(e.target.files);
|
selectedFiles = Array.from(e.target.files);
|
||||||
|
|
||||||
if (selectedFiles.length > 0) {
|
if (selectedFiles.length > 0) {
|
||||||
@@ -1299,7 +1357,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
selectedFiles.forEach((file, index) => {
|
selectedFiles.forEach((file, index) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function (event) {
|
||||||
const col = document.createElement('div');
|
const col = document.createElement('div');
|
||||||
col.className = 'col-4 col-md-3 col-lg-2';
|
col.className = 'col-4 col-md-3 col-lg-2';
|
||||||
col.innerHTML = `
|
col.innerHTML = `
|
||||||
@@ -1321,7 +1379,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removePhoto = function(index) {
|
window.removePhoto = function (index) {
|
||||||
selectedFiles.splice(index, 1);
|
selectedFiles.splice(index, 1);
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
selectedFiles.forEach(file => dataTransfer.items.add(file));
|
selectedFiles.forEach(file => dataTransfer.items.add(file));
|
||||||
@@ -1413,7 +1471,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Обработчик для кнопки "Пополнить базу названиям<D18F><D0BC>"
|
// Обработчик для кнопки "Пополнить базу названиям<D18F><D0BC>"
|
||||||
const populateNamesBtn = document.getElementById('populateNamesBtn');
|
const populateNamesBtn = document.getElementById('populateNamesBtn');
|
||||||
if (populateNamesBtn) {
|
if (populateNamesBtn) {
|
||||||
populateNamesBtn.addEventListener('click', async function() {
|
populateNamesBtn.addEventListener('click', async function () {
|
||||||
const originalHTML = populateNamesBtn.innerHTML;
|
const originalHTML = populateNamesBtn.innerHTML;
|
||||||
populateNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Пополнение...';
|
populateNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Пополнение...';
|
||||||
populateNamesBtn.disabled = true;
|
populateNamesBtn.disabled = true;
|
||||||
@@ -1425,7 +1483,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({'count': 100})
|
body: new URLSearchParams({ 'count': 100 })
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -1512,7 +1570,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.querySelectorAll('.btn-take-name').forEach(button => {
|
document.querySelectorAll('.btn-take-name').forEach(button => {
|
||||||
// Проверяем, был ли уже добавлен обработчик
|
// Проверяем, был ли уже добавлен обработчик
|
||||||
if (!button.dataset.handlerAttached) {
|
if (!button.dataset.handlerAttached) {
|
||||||
button.addEventListener('click', async function() {
|
button.addEventListener('click', async function () {
|
||||||
const row = this.closest('.name-row');
|
const row = this.closest('.name-row');
|
||||||
const nameText = row.querySelector('.name-text').textContent;
|
const nameText = row.querySelector('.name-text').textContent;
|
||||||
const nameId = row.getAttribute('data-name-id');
|
const nameId = row.getAttribute('data-name-id');
|
||||||
@@ -1546,7 +1604,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.querySelectorAll('.btn-delete-name').forEach(button => {
|
document.querySelectorAll('.btn-delete-name').forEach(button => {
|
||||||
// Проверяем, был ли уже добавлен обработчик
|
// Проверяем, был ли уже добавлен обработчик
|
||||||
if (!button.dataset.handlerAttached) {
|
if (!button.dataset.handlerAttached) {
|
||||||
button.addEventListener('click', async function() {
|
button.addEventListener('click', async function () {
|
||||||
const row = this.closest('.name-row');
|
const row = this.closest('.name-row');
|
||||||
const nameId = row.getAttribute('data-name-id');
|
const nameId = row.getAttribute('data-name-id');
|
||||||
|
|
||||||
@@ -1632,7 +1690,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация обработчиков кнопок
|
// Инициализация обработчиков кнопок
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function (e) {
|
||||||
if (e.target.classList.contains('btn-take-name') || e.target.classList.contains('btn-delete-name')) {
|
if (e.target.classList.contains('btn-take-name') || e.target.classList.contains('btn-delete-name')) {
|
||||||
attachNameRowHandlers();
|
attachNameRowHandlers();
|
||||||
}
|
}
|
||||||
@@ -1641,7 +1699,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
|
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
|
||||||
const kitForm = document.querySelector('form[method="post"]');
|
const kitForm = document.querySelector('form[method="post"]');
|
||||||
if (kitForm) {
|
if (kitForm) {
|
||||||
kitForm.addEventListener('submit', function(e) {
|
kitForm.addEventListener('submit', function (e) {
|
||||||
const formsContainer = document.getElementById('kititem-forms');
|
const formsContainer = document.getElementById('kititem-forms');
|
||||||
if (formsContainer) {
|
if (formsContainer) {
|
||||||
const allForms = formsContainer.querySelectorAll('.kititem-form');
|
const allForms = formsContainer.querySelectorAll('.kititem-form');
|
||||||
@@ -1671,6 +1729,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
allSelects.forEach(select => select.disabled = false);
|
allSelects.forEach(select => select.disabled = false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -506,6 +506,9 @@
|
|||||||
<a href="{% url 'products:productkit-detail' object.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'products:productkit-detail' object.pk %}" class="btn btn-outline-secondary">
|
||||||
Отмена
|
Отмена
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'products:productkit-create' %}?copy_from={{ object.pk }}" class="btn btn-warning text-white mx-2">
|
||||||
|
<i class="bi bi-files me-1"></i>Копировать комплект
|
||||||
|
</a>
|
||||||
<button type="submit" class="btn btn-primary px-4">
|
<button type="submit" class="btn btn-primary px-4">
|
||||||
<i class="bi bi-check-circle me-1"></i>Сохранить изменения
|
<i class="bi bi-check-circle me-1"></i>Сохранить изменения
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.shortcuts import redirect
|
|||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
|
|
||||||
from user_roles.mixins import ManagerOwnerRequiredMixin
|
from user_roles.mixins import ManagerOwnerRequiredMixin
|
||||||
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup, BouquetName
|
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup, BouquetName, ProductSalesUnit
|
||||||
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
||||||
from .utils import handle_photos
|
from .utils import handle_photos
|
||||||
|
|
||||||
@@ -97,6 +97,28 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
form_class = ProductKitForm
|
form_class = ProductKitForm
|
||||||
template_name = 'products/productkit_create.html'
|
template_name = 'products/productkit_create.html'
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
copy_id = self.request.GET.get('copy_from')
|
||||||
|
if copy_id:
|
||||||
|
try:
|
||||||
|
kit = ProductKit.objects.get(pk=copy_id)
|
||||||
|
initial.update({
|
||||||
|
'name': f"{kit.name} (Копия)",
|
||||||
|
'description': kit.description,
|
||||||
|
'short_description': kit.short_description,
|
||||||
|
'categories': list(kit.categories.values_list('pk', flat=True)),
|
||||||
|
'tags': list(kit.tags.values_list('pk', flat=True)),
|
||||||
|
'sale_price': kit.sale_price,
|
||||||
|
'price_adjustment_type': kit.price_adjustment_type,
|
||||||
|
'price_adjustment_value': kit.price_adjustment_value,
|
||||||
|
'external_category': kit.external_category,
|
||||||
|
'status': 'active', # Default to active for new kits
|
||||||
|
})
|
||||||
|
except ProductKit.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return initial
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
|
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
|
||||||
@@ -132,7 +154,6 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem')
|
||||||
|
|
||||||
# При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2
|
# При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2
|
||||||
from ..models import Product, ProductVariantGroup, ProductSalesUnit
|
|
||||||
selected_products = {}
|
selected_products = {}
|
||||||
selected_variants = {}
|
selected_variants = {}
|
||||||
selected_sales_units = {}
|
selected_sales_units = {}
|
||||||
@@ -194,9 +215,88 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
context['selected_products'] = selected_products
|
context['selected_products'] = selected_products
|
||||||
context['selected_variants'] = selected_variants
|
context['selected_variants'] = selected_variants
|
||||||
context['selected_sales_units'] = selected_sales_units
|
context['selected_sales_units'] = selected_sales_units
|
||||||
|
else:
|
||||||
|
# COPY KIT LOGIC
|
||||||
|
copy_id = self.request.GET.get('copy_from')
|
||||||
|
initial_items = []
|
||||||
|
selected_products = {}
|
||||||
|
selected_variants = {}
|
||||||
|
selected_sales_units = {}
|
||||||
|
|
||||||
|
if copy_id:
|
||||||
|
try:
|
||||||
|
source_kit = ProductKit.objects.get(pk=copy_id)
|
||||||
|
for item in source_kit.kit_items.all():
|
||||||
|
item_data = {
|
||||||
|
'quantity': item.quantity,
|
||||||
|
# Delete flag is false by default
|
||||||
|
}
|
||||||
|
|
||||||
|
form_prefix = f"kititem-{len(initial_items)}"
|
||||||
|
|
||||||
|
if item.product:
|
||||||
|
item_data['product'] = item.product
|
||||||
|
# Select2 prefill
|
||||||
|
product = item.product
|
||||||
|
text = product.name
|
||||||
|
if product.sku:
|
||||||
|
text += f" ({product.sku})"
|
||||||
|
actual_price = product.sale_price if product.sale_price else product.price
|
||||||
|
selected_products[f"{form_prefix}-product"] = {
|
||||||
|
'id': product.id,
|
||||||
|
'text': text,
|
||||||
|
'price': str(product.price) if product.price else None,
|
||||||
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.sales_unit:
|
||||||
|
item_data['sales_unit'] = item.sales_unit
|
||||||
|
# Select2 prefill
|
||||||
|
sales_unit = item.sales_unit
|
||||||
|
text = f"{sales_unit.name} ({sales_unit.product.name})"
|
||||||
|
actual_price = sales_unit.sale_price if sales_unit.sale_price else sales_unit.price
|
||||||
|
selected_sales_units[f"{form_prefix}-sales_unit"] = {
|
||||||
|
'id': sales_unit.id,
|
||||||
|
'text': text,
|
||||||
|
'price': str(sales_unit.price) if sales_unit.price else None,
|
||||||
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.variant_group:
|
||||||
|
item_data['variant_group'] = item.variant_group
|
||||||
|
# Select2 prefill
|
||||||
|
variant_group = ProductVariantGroup.objects.prefetch_related(
|
||||||
|
'items__product'
|
||||||
|
).get(id=item.variant_group.id)
|
||||||
|
variant_price = variant_group.price or 0
|
||||||
|
count = variant_group.items.count()
|
||||||
|
selected_variants[f"{form_prefix}-variant_group"] = {
|
||||||
|
'id': variant_group.id,
|
||||||
|
'text': f"{variant_group.name} ({count} вариантов)",
|
||||||
|
'price': str(variant_price),
|
||||||
|
'actual_price': str(variant_price),
|
||||||
|
'type': 'variant',
|
||||||
|
'count': count
|
||||||
|
}
|
||||||
|
|
||||||
|
initial_items.append(item_data)
|
||||||
|
except ProductKit.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if initial_items:
|
||||||
|
context['kititem_formset'] = KitItemFormSetCreate(
|
||||||
|
prefix='kititem',
|
||||||
|
initial=initial_items
|
||||||
|
)
|
||||||
|
context['kititem_formset'].extra = len(initial_items)
|
||||||
else:
|
else:
|
||||||
context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem')
|
||||||
|
|
||||||
|
# Pass Select2 data to context
|
||||||
|
context['selected_products'] = selected_products
|
||||||
|
context['selected_variants'] = selected_variants
|
||||||
|
context['selected_sales_units'] = selected_sales_units
|
||||||
|
|
||||||
# Количество названий букетов в базе
|
# Количество названий букетов в базе
|
||||||
context['bouquet_names_count'] = BouquetName.objects.count()
|
context['bouquet_names_count'] = BouquetName.objects.count()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user