feat(inventory): добавить столбец текущей цены продажи и inline-редактирование количества

- Добавлен столбец "Текущая цена продажи" в таблицу позиций документа поступления
- Цена продажи отображается с учётом скидок и возможностью inline-редактирования для владельца и менеджера
- Реализовано inline-редактирование количества при клике на значение
- Добавлены стили для интерактивных элементов

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 19:54:35 +03:00
parent ffc5f4cfc1
commit 622c544182

View File

@@ -188,6 +188,7 @@
<th scope="col" class="px-3 py-2 text-end" style="width: 120px;">Закупочная цена</th>
<th scope="col" class="px-3 py-2 text-end" style="width: 120px;">Сумма</th>
<th scope="col" class="px-3 py-2">Примечания</th>
<th scope="col" class="px-3 py-2 text-end" style="width: 120px;">Текущая цена продажи</th>
{% if document.can_edit %}
<th scope="col" class="px-3 py-2" style="width: 100px;"></th>
{% endif %}
@@ -200,11 +201,19 @@
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
</td>
<td class="px-3 py-2 text-end" style="width: 120px;">
<span class="item-quantity-display">{{ item.quantity|smart_quantity }}</span>
{% if document.can_edit %}
<span class="editable-quantity"
data-item-id="{{ item.id }}"
data-current-value="{{ item.quantity }}"
title="Количество (клик для редактирования)"
style="cursor: pointer;">
{{ item.quantity|smart_quantity }}
</span>
<input type="number" class="form-control form-control-sm item-quantity-input"
value="{{ item.quantity|stringformat:'g' }}" step="0.001" min="0.001"
style="display: none; width: 100px; text-align: right; margin-left: auto;">
{% else %}
<span>{{ item.quantity|smart_quantity }}</span>
{% endif %}
</td>
<td class="px-3 py-2 text-end" style="width: 120px;">
@@ -226,6 +235,40 @@
style="display: none;">
{% endif %}
</td>
<td class="px-3 py-2 text-end" style="width: 120px;">
{% if item.product.sale_price %}
<div class="text-decoration-line-through text-muted small">{{ item.product.price|floatformat:2 }} руб.</div>
{% if user.is_superuser or user.tenant_role.role.code == 'owner' or user.tenant_role.role.code == 'manager' %}
<strong class="text-danger editable-price"
data-product-id="{{ item.product.pk }}"
data-field="sale_price"
data-current-value="{{ item.product.sale_price }}"
title="Цена со скидкой (клик для редактирования)"
style="cursor: pointer;">
{{ item.product.sale_price|floatformat:2 }} руб.
</strong>
{% else %}
<strong class="text-danger">
{{ item.product.sale_price|floatformat:2 }} руб.
</strong>
{% endif %}
{% else %}
{% if user.is_superuser or user.tenant_role.role.code == 'owner' or user.tenant_role.role.code == 'manager' %}
<strong class="editable-price"
data-product-id="{{ item.product.pk }}"
data-field="price"
data-current-value="{{ item.product.price }}"
title="Цена продажи (клик для редактирования)"
style="cursor: pointer;">
{{ item.product.price|floatformat:2 }} руб.
</strong>
{% else %}
<strong>
{{ item.product.price|floatformat:2 }} руб.
</strong>
{% endif %}
{% endif %}
</td>
{% if document.can_edit %}
<td class="px-3 py-2 text-end" style="width: 100px;">
<div class="btn-group btn-group-sm item-action-buttons">
@@ -256,7 +299,7 @@
</tr>
{% empty %}
<tr>
<td colspan="{% if document.can_edit %}6{% else %}5{% endif %}" class="px-3 py-4 text-center text-muted">
<td colspan="{% if document.can_edit %}7{% else %}6{% endif %}" class="px-3 py-4 text-center text-muted">
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
Позиций пока нет
</td>
@@ -268,7 +311,7 @@
<tr>
<td class="px-3 py-2 fw-semibold">Итого:</td>
<td class="px-3 py-2 fw-semibold text-end">{{ document.total_quantity|smart_quantity }}</td>
<td colspan="2" class="px-3 py-2 fw-semibold text-end">{{ document.total_cost|floatformat:2 }}</td>
<td colspan="3" class="px-3 py-2 fw-semibold text-end">{{ document.total_cost|floatformat:2 }}</td>
<td colspan="{% if document.can_edit %}2{% else %}1{% endif %}"></td>
</tr>
</tfoot>
@@ -283,6 +326,7 @@
<!-- JS для компонента поиска -->
<script src="{% static 'products/js/product-search-picker.js' %}?v=3"></script>
<script src="{% static 'products/js/inline-price-edit.js' %}?v=1.5"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Элементы формы
@@ -523,7 +567,184 @@ document.addEventListener('DOMContentLoaded', function() {
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
});
}
// ============================================
// Inline редактирование количества
// ============================================
function initInlineQuantityEdit() {
// Проверяем, есть ли на странице редактируемые количества
const editableQuantities = document.querySelectorAll('.editable-quantity');
if (editableQuantities.length === 0) {
return; // Нет элементов для редактирования
}
// Обработчик клика на редактируемое количество
document.addEventListener('click', function(e) {
const quantitySpan = e.target.closest('.editable-quantity');
if (!quantitySpan) return;
// Предотвращаем повторное срабатывание, если уже редактируем
if (quantitySpan.querySelector('input')) return;
const itemId = quantitySpan.dataset.itemId;
const currentValue = quantitySpan.dataset.currentValue;
// Сохраняем оригинальный HTML
const originalHTML = quantitySpan.innerHTML;
// Создаем input для редактирования
const input = document.createElement('input');
input.type = 'number';
input.className = 'form-control form-control-sm';
input.style.width = '100px';
input.style.textAlign = 'right';
input.value = parseFloat(currentValue).toFixed(3);
input.step = '0.001';
input.min = '0.001';
input.placeholder = 'Количество';
// Заменяем содержимое на input
quantitySpan.innerHTML = '';
quantitySpan.appendChild(input);
input.focus();
input.select();
// Функция сохранения
const saveQuantity = async () => {
let newValue = input.value.trim();
// Валидация
if (!newValue || parseFloat(newValue) <= 0) {
alert('Количество должно быть больше нуля');
quantitySpan.innerHTML = originalHTML;
return;
}
// Проверяем, изменилось ли значение
if (parseFloat(newValue) === parseFloat(currentValue)) {
// Значение не изменилось
quantitySpan.innerHTML = originalHTML;
return;
}
// Показываем загрузку
input.disabled = true;
input.style.opacity = '0.5';
try {
// Получаем текущие значения других полей
const row = quantitySpan.closest('tr');
const costPrice = row.querySelector('.item-cost-price-input').value;
const notes = row.querySelector('.item-notes-input').value;
const response = await fetch(`/inventory/incoming-documents/{{ document.pk }}/update-item/${itemId}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: new URLSearchParams({
quantity: newValue,
cost_price: costPrice,
notes: notes
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.success) {
// Обновляем отображение
let formattedQty = parseFloat(newValue);
if (formattedQty === Math.floor(formattedQty)) {
formattedQty = Math.floor(formattedQty).toString();
} else {
formattedQty = formattedQty.toString().replace('.', ',');
}
quantitySpan.textContent = formattedQty;
quantitySpan.dataset.currentValue = newValue;
// Пересчитываем сумму
const totalCost = (parseFloat(newValue) * parseFloat(costPrice)).toFixed(2);
row.querySelector('td:nth-child(4) strong').textContent = totalCost;
// Обновляем итого
updateTotals();
} else {
alert(data.error || 'Ошибка при обновлении количества');
quantitySpan.innerHTML = originalHTML;
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка сети при обновлении количества');
quantitySpan.innerHTML = originalHTML;
}
};
// Функция отмены
const cancelEdit = () => {
quantitySpan.innerHTML = originalHTML;
};
// Enter - сохранить
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
saveQuantity();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
// Потеря фокуса - сохранить
input.addEventListener('blur', function() {
setTimeout(saveQuantity, 100);
});
});
}
// Функция обновления итоговых сумм
function updateTotals() {
// Можно реализовать пересчет итогов, если нужно
// Пока оставим как есть, так как сервер возвращает обновленные данные
}
// Инициализация inline редактирования количества
initInlineQuantityEdit();
});
</script>
<style>
/* Стили для редактируемых цен */
.editable-price {
cursor: pointer;
transition: color 0.2s ease;
}
.editable-price:hover {
color: #0d6efd !important;
text-decoration: underline;
}
.price-edit-container {
min-height: 2.5rem;
}
/* Стили для редактируемого количества */
.editable-quantity {
cursor: pointer;
transition: color 0.2s ease;
}
.editable-quantity:hover {
color: #0d6efd !important;
text-decoration: underline;
}
</style>
{% endblock %}