feat(inventory): добавить столбец текущей цены продажи и inline-редактирование количества
- Добавлен столбец "Текущая цена продажи" в таблицу позиций документа поступления - Цена продажи отображается с учётом скидок и возможностью inline-редактирования для владельца и менеджера - Реализовано inline-редактирование количества при клике на значение - Добавлены стили для интерактивных элементов Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 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">Примечания</th>
|
||||||
|
<th scope="col" class="px-3 py-2 text-end" style="width: 120px;">Текущая цена продажи</th>
|
||||||
{% if document.can_edit %}
|
{% if document.can_edit %}
|
||||||
<th scope="col" class="px-3 py-2" style="width: 100px;"></th>
|
<th scope="col" class="px-3 py-2" style="width: 100px;"></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -200,11 +201,19 @@
|
|||||||
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
|
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-end" style="width: 120px;">
|
<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 %}
|
{% if document.can_edit %}
|
||||||
<input type="number" class="form-control form-control-sm item-quantity-input"
|
<span class="editable-quantity"
|
||||||
value="{{ item.quantity|stringformat:'g' }}" step="0.001" min="0.001"
|
data-item-id="{{ item.id }}"
|
||||||
style="display: none; width: 100px; text-align: right; margin-left: auto;">
|
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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-end" style="width: 120px;">
|
<td class="px-3 py-2 text-end" style="width: 120px;">
|
||||||
@@ -226,6 +235,40 @@
|
|||||||
style="display: none;">
|
style="display: none;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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 %}
|
{% if document.can_edit %}
|
||||||
<td class="px-3 py-2 text-end" style="width: 100px;">
|
<td class="px-3 py-2 text-end" style="width: 100px;">
|
||||||
<div class="btn-group btn-group-sm item-action-buttons">
|
<div class="btn-group btn-group-sm item-action-buttons">
|
||||||
@@ -256,7 +299,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<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>
|
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
|
||||||
Позиций пока нет
|
Позиций пока нет
|
||||||
</td>
|
</td>
|
||||||
@@ -268,7 +311,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="px-3 py-2 fw-semibold">Итого:</td>
|
<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 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>
|
<td colspan="{% if document.can_edit %}2{% else %}1{% endif %}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -283,6 +326,7 @@
|
|||||||
|
|
||||||
<!-- JS для компонента поиска -->
|
<!-- JS для компонента поиска -->
|
||||||
<script src="{% static 'products/js/product-search-picker.js' %}?v=3"></script>
|
<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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Элементы формы
|
// Элементы формы
|
||||||
@@ -523,7 +567,184 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
|
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>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user