fix: Улучшения системы ценообразования комплектов

Исправлены 4 проблемы:
1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice
2. Отображение actual_price в Select2 вместо обычной цены
3. Количество по умолчанию = 1 для новых форм компонентов
4. Auto-select текста при клике на поле количества для удобства редактирования

Изменённые файлы:
- products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1
- products/templates/includes/select2-product-init.html: обновлена formatSelectResult
- products/templates/productkit_create.html: добавлен focus handler для auto-select
- products/templates/productkit_edit.html: добавлен focus handler для auto-select

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-02 19:04:03 +03:00
parent c84a372f98
commit 6c8af5ab2c
120 changed files with 9035 additions and 3036 deletions

View File

@@ -1,24 +1,31 @@
{% extends 'inventory/base_inventory.html' %}
{% block inventory_title %}Удаление склада{% endblock %}
{% block inventory_title %}Архивирование склада{% endblock %}
{% block inventory_content %}
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">Подтверждение удаления</h4>
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h4 class="mb-0">Архивирование склада</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
<strong>Внимание!</strong> Вы собираетесь удалить (деактивировать) склад.
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Вы собираетесь архивировать склад <strong>"{{ warehouse.name }}"</strong>
</div>
<p class="text-muted">
Этот склад будет деактивирован и скрыт из основного списка.
<p>
<strong>Что произойдет после архивирования:</strong>
</p>
<ul>
<li>✓ Склад исчезнет из списка активных складов</li>
<li>✓ Новые документы нельзя будет создавать для этого склада</li>
<li>✓ Историю операций можно будет посмотреть в архиве</li>
</ul>
<h5>Склад: <strong>{{ warehouse.name }}</strong></h5>
<div class="alert alert-secondary mt-3">
<small>Вы всегда сможете вернуть склад, отредактировав его позже.</small>
</div>
{% if warehouse.description %}
<p class="text-muted">{{ warehouse.description }}</p>
@@ -28,8 +35,8 @@
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Подтвердить удаление
<button type="submit" class="btn btn-warning">
<i class="bi bi-archive"></i> Архивировать
</button>
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отменить

View File

@@ -67,6 +67,20 @@
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="{{ form.is_default.id_for_label }}" name="{{ form.is_default.html_name }}"
{% if form.is_default.value %}checked{% endif %}>
<label class="form-check-label" for="{{ form.is_default.id_for_label }}">
{{ form.is_default.label }}
<small class="text-muted d-block">
Отмечьте, чтобы использовать этот склад по умолчанию при создании новых документов
</small>
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i>

View File

@@ -3,6 +3,10 @@
{% block inventory_title %}Управление складами{% endblock %}
{% block inventory_content %}
<!-- Скрытое поле для CSRF токена (нужно для AJAX запросов) -->
<div style="display: none;">
{% csrf_token %}
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">Список складов</h4>
@@ -17,6 +21,7 @@
<table class="table table-hover">
<thead class="table-light">
<tr>
<th style="width: 40px;"></th>
<th>Название</th>
<th>Описание</th>
<th>Статус</th>
@@ -26,8 +31,20 @@
</thead>
<tbody>
{% for warehouse in warehouses %}
<tr>
<td><strong>{{ warehouse.name }}</strong></td>
<tr {% if warehouse.is_default %}class="table-warning"{% endif %} data-warehouse-id="{{ warehouse.pk }}">
<td class="text-center">
<input type="checkbox" class="default-warehouse-checkbox"
data-warehouse-id="{{ warehouse.pk }}"
data-set-default-url="{% url 'inventory:warehouse-set-default' warehouse.pk %}"
{% if warehouse.is_default %}checked{% endif %}
style="cursor: pointer; width: 18px; height: 18px;">
</td>
<td>
<strong>{{ warehouse.name }}</strong>
{% if warehouse.is_default %}
<span class="badge bg-warning text-dark ms-2">По умолчанию</span>
{% endif %}
</td>
<td>{{ warehouse.description|truncatewords:10 }}</td>
<td>
{% if warehouse.is_active %}
@@ -95,4 +112,145 @@
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Обработчик для галочек "По умолчанию"
const checkboxes = document.querySelectorAll('.default-warehouse-checkbox');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const warehouseId = this.dataset.warehouseId;
const setDefaultUrl = this.dataset.setDefaultUrl;
// Получаем CSRF токен из скрытого input в форме
let csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
// Если токена нет в form, ищем в meta тегах
if (!csrfToken) {
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
}
// Если токена еще нет, ищем его в самой странице через Cookies
if (!csrfToken) {
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
csrfToken = cookieValue;
}
console.log('CSRF Token:', csrfToken ? 'найден (' + csrfToken.length + ' символов)' : 'не найден');
// Если галочка установлена, отправляем запрос
if (this.checked) {
// Визуально обновляем таблицу сразу (оптимистичное обновление)
document.querySelectorAll('input.default-warehouse-checkbox').forEach(cb => {
cb.checked = false;
});
document.querySelectorAll('tr[data-warehouse-id]').forEach(tr => {
tr.classList.remove('table-warning');
tr.querySelector('.badge.bg-warning')?.remove();
});
// Отмечаем текущую строку
this.checked = true;
const currentRow = document.querySelector(`tr[data-warehouse-id="${warehouseId}"]`);
currentRow.classList.add('table-warning');
// Добавляем бейдж "По умолчанию" если его нет
const nameCell = currentRow.querySelector('td:nth-child(2)');
if (!nameCell.querySelector('.badge.bg-warning')) {
const badge = document.createElement('span');
badge.className = 'badge bg-warning text-dark ms-2';
badge.textContent = 'По умолчанию';
nameCell.appendChild(badge);
}
// Отправляем AJAX запрос на правильный URL из атрибута data
console.log('Отправляем запрос на:', setDefaultUrl);
console.log('С заголовками:', {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken ? '***' + csrfToken.slice(-10) : 'не найден'
});
const headers = {
'Content-Type': 'application/json'
};
// Добавляем CSRF токен если он найден
if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
}
fetch(setDefaultUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify({})
})
.then(response => {
console.log('Ответ сервера:', response.status);
if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP ${response.status}: ${text}`);
});
}
return response.json();
})
.then(data => {
console.log('Данные:', data);
if (data.status === 'success') {
console.log(data.message);
// Показываем уведомление
showNotification(data.message, 'success');
} else {
throw new Error(data.message);
}
})
.catch(error => {
console.error('Ошибка при запросе:', error);
// Откатываем визуальные изменения при ошибке
showNotification('Ошибка при установке склада по умолчанию: ' + error.message, 'error');
// Перезагружаем через 2 секунды
setTimeout(() => {
location.reload();
}, 2000);
});
}
});
});
// Функция для показа уведомлений
function showNotification(message, type = 'info') {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
const cardBody = document.querySelector('.card-body');
const alertElement = document.createElement('div');
alertElement.innerHTML = alertHtml;
cardBody.insertBefore(alertElement.firstElementChild, cardBody.firstChild);
// Автоматически скрываем через 4 секунды
setTimeout(() => {
const alert = cardBody.querySelector('.alert');
if (alert) {
alert.remove();
}
}, 4000);
}
});
</script>
{% endblock %}