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">Примечания</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,18 +201,26 @@
|
||||
<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 %}
|
||||
<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;">
|
||||
<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;">
|
||||
<span class="item-cost-price-display">{{ item.cost_price|floatformat:2 }}</span>
|
||||
{% if document.can_edit %}
|
||||
<input type="number" class="form-control form-control-sm item-cost-price-input"
|
||||
value="{{ item.cost_price|stringformat:'g' }}" step="0.01" min="0"
|
||||
<input type="number" class="form-control form-control-sm item-cost-price-input"
|
||||
value="{{ item.cost_price|stringformat:'g' }}" step="0.01" min="0"
|
||||
style="display: none; width: 100px; text-align: right; margin-left: auto;">
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -221,11 +230,45 @@
|
||||
<td class="px-3 py-2">
|
||||
<span class="item-notes-display">{% if item.notes %}{{ item.notes|truncatechars:50 }}{% else %}-{% endif %}</span>
|
||||
{% if document.can_edit %}
|
||||
<input type="text" class="form-control form-control-sm item-notes-input"
|
||||
value="{{ item.notes }}" placeholder="Примечания"
|
||||
<input type="text" class="form-control form-control-sm item-notes-input"
|
||||
value="{{ item.notes }}" placeholder="Примечания"
|
||||
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 %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user