refactor: мигрировать на новую систему документов поступления

Удалена старая одноэтапная система incoming и оставлена только новая
двухэтапная система IncomingDocument (черновик → проведение).

Изменения:
- URL структура изменена с /incoming-documents/ на /incoming/
- URL names: incoming-document-* → incoming-*
- Удалены старые views, forms, templates для Incoming/IncomingBatch
- Обновлена навигация и все ссылки в шаблонах
- Модели IncomingBatch/Incoming сохранены как внутренняя архитектура

Удалено ~1590 строк кода:
- inventory/views/incoming.py (389 строк)
- inventory/forms.py (206 строк старых форм)
- inventory/admin.py (56 строк)
- 4 шаблона incoming/*.html (895 строк)

Обновлено:
- inventory/urls.py - новая URL структура
- inventory/views/incoming_document.py - обновлены redirects
- Все шаблоны с ссылками на incoming

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 17:33:00 +03:00
parent d2384394c8
commit c9ff778630
16 changed files with 30 additions and 1590 deletions

View File

@@ -23,9 +23,7 @@
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Управление</h6></li>
<li><a class="dropdown-item" href="{% url 'inventory:warehouse-list' %}">Склады</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:incoming-list' %}">Приходы</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:incoming-create' %}">Поступление товара</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:incoming-adjustment-create' %}">Оприходование</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:incoming-list' %}">Поступления</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:sale-list' %}">Продажи</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:inventory-list' %}">Инвентаризация</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:writeoff-list' %}">Списания</a></li>

View File

@@ -127,7 +127,7 @@
<!-- Документы поступления -->
<div class="col-md-6 col-lg-4">
<a href="{% url 'inventory:incoming-document-list' %}" class="card shadow-sm h-100 text-decoration-none">
<a href="{% url 'inventory:incoming-list' %}" class="card shadow-sm h-100 text-decoration-none">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-success bg-opacity-10 p-3 me-3">

View File

@@ -1,585 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}{% if is_adjustment %}Оприходование товара{% else %}Массовое поступление товара{% endif %}{% endblock %}
{% block breadcrumb_current %}Приходы{% endblock %}
{% block inventory_content %}
<div class="card">
<div class="card-header">
<h4 class="mb-0">{% if is_adjustment %}Оприходование товара{% else %}Поступление товара от поставщика{% endif %}</h4>
</div>
<div class="card-body">
<!-- Ошибки общей формы -->
{% if form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>❌ Ошибка:</strong>
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<form method="post" novalidate id="bulkIncomingForm">
{% csrf_token %}
<!-- ============== HEADER ИНФОРМАЦИЯ ============== -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{{ form.warehouse.label }} <span class="text-danger">*</span></label>
{{ form.warehouse }}
{% if form.warehouse.errors %}
<div class="text-danger small">{{ form.warehouse.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{{ form.document_number.label }}</label>
{{ form.document_number }}
{% if form.document_number.errors %}
<div class="text-danger small">{{ form.document_number.errors.0 }}</div>
{% endif %}
<small class="text-muted d-block mt-1">
Оставьте пустым для автогенерации свободного номера (формат: IN-XXXX-XXXX). Номера, начинающиеся с IN-, зарезервированы для системы.
</small>
</div>
</div>
</div>
{% if not is_adjustment %}
<div class="mb-3">
<label class="form-label">{{ form.supplier_name.label }}</label>
{{ form.supplier_name }}
{% if form.supplier_name.errors %}
<div class="text-danger small">{{ form.supplier_name.errors.0 }}</div>
{% endif %}
</div>
{% endif %}
{{ form.receipt_type }}
<div class="mb-3">
<label class="form-label">{{ form.notes.label }}</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
{% endif %}
</div>
<hr>
<!-- ============== ТАБЛИЦА ТОВАРОВ ============== -->
<div class="mb-3">
<h5>Товары в поступлении</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered" id="productsTable">
<thead class="table-light">
<tr>
<th style="width: 45%;">Товар</th>
<th style="width: 15%;">Кол-во (шт)</th>
<th style="width: 15%;">Цена закупки</th>
<th style="width: 15%;">Сумма</th>
<th style="width: 10%;">Действие</th>
</tr>
</thead>
<tbody id="productsBody">
<!-- Строки будут добавлены через JavaScript -->
</tbody>
</table>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addRowBtn">
<i class="bi bi-plus-circle"></i> Добавить товар
</button>
</div>
<!-- ============== ИТОГО ============== -->
<div class="row mb-3">
<div class="col-md-6"></div>
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Кол-во позиций:</strong></div>
<div class="col-6 text-end"><span id="totalItems">0</span></div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Общее количество:</strong></div>
<div class="col-6 text-end"><span id="totalQuantity">0</span> шт</div>
</div>
<div class="row">
<div class="col-6"><strong>Сумма поступления:</strong></div>
<div class="col-6 text-end text-primary"><strong><span id="totalSum">0.00</span> руб</strong></div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden input для JSON данных товаров -->
<input type="hidden" id="productsJson" name="products_json" value="[]">
<!-- ============== КНОПКИ ============== -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="bi bi-check-circle"></i> {% if is_adjustment %}Создать оприходование{% else %}Создать поступление{% endif %}
</button>
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
</div>
</form>
</div>
</div>
<style>
select, textarea, input {
width: 100%;
}
.table-responsive {
border-radius: 4px;
border: 1px solid #dee2e6;
}
.btn-remove-row {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
input[readonly] {
background-color: #e9ecef;
}
.row-error {
background-color: #fff5f5;
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.is-invalid {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('bulkIncomingForm');
const productsBody = document.getElementById('productsBody');
const addRowBtn = document.getElementById('addRowBtn');
const productsJsonInput = document.getElementById('productsJson');
const submitBtn = document.getElementById('submitBtn');
let rowCounter = 0;
// Добавление новой строки товара
addRowBtn.addEventListener('click', function(e) {
e.preventDefault();
addProductRow();
});
function addProductRow() {
rowCounter++;
const rowId = `row-${rowCounter}`;
const row = document.createElement('tr');
row.id = rowId;
row.innerHTML = `
<td>
<select class="form-control form-control-sm product-select" data-row-id="${rowId}">
<option value="">Начните вводить название товара...</option>
</select>
<div class="error-message" style="display:none;"></div>
</td>
<td>
<input type="number" class="form-control form-control-sm quantity-input"
data-row-id="${rowId}" step="0.001" placeholder="0" min="0">
<div class="error-message" style="display:none;"></div>
</td>
<td>
<input type="number" class="form-control form-control-sm price-input"
data-row-id="${rowId}" step="0.01" placeholder="0.00" min="0">
<div class="error-message" style="display:none;"></div>
</td>
<td>
<input type="text" class="form-control form-control-sm sum-display"
data-row-id="${rowId}" readonly style="text-align:right;">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger btn-remove-row" data-row-id="${rowId}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
productsBody.appendChild(row);
// Инициализируем Select2 для нового селекта товара
const productSelect = row.querySelector('.product-select');
$(productSelect).select2({
theme: 'bootstrap-5',
placeholder: 'Начните вводить название товара...',
allowClear: true,
width: '100%',
language: 'ru',
minimumInputLength: 0,
ajax: {
url: '/products/api/search-products-variants/',
dataType: 'json',
delay: 250,
data: function (params) {
return {
q: params.term || '',
type: 'product',
page: params.page || 1
};
},
processResults: function (data) {
// Преобразуем результаты: извлекаем числовые ID из 'product_123'
const processGroup = function(group) {
if (group.children) {
// Это группа - обрабатываем детей
return {
text: group.text,
children: group.children.map(item => ({
id: item.id.replace('product_', ''), // Убираем префикс
text: item.text,
sku: item.sku,
price: item.price,
actual_price: item.actual_price
}))
};
} else {
// Это отдельный элемент
return {
id: group.id.replace('product_', ''),
text: group.text,
sku: group.sku,
price: group.price,
actual_price: group.actual_price
};
}
};
const processedResults = data.results.map(processGroup);
return {
results: processedResults,
pagination: {
more: data.pagination.more
}
};
},
cache: true
}
});
// Добавляем event listeners для новой строки
const quantityInput = row.querySelector('.quantity-input');
const priceInput = row.querySelector('.price-input');
const removeBtn = row.querySelector('.btn-remove-row');
$(productSelect).on('change', function() {
productSelect.classList.remove('is-invalid');
updateTotals();
});
quantityInput.addEventListener('input', function() {
quantityInput.classList.remove('is-invalid');
updateTotals();
});
priceInput.addEventListener('input', function() {
priceInput.classList.remove('is-invalid');
updateTotals();
});
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
$(productSelect).select2('destroy');
row.remove();
updateTotals();
});
updateTotals();
}
function updateTotals() {
let totalItems = 0;
let totalQuantity = 0;
let totalSum = 0;
const productsData = [];
productsBody.querySelectorAll('tr').forEach(row => {
const productSelect = row.querySelector('.product-select');
const quantityInput = row.querySelector('.quantity-input');
const priceInput = row.querySelector('.price-input');
const sumDisplay = row.querySelector('.sum-display');
const productId = productSelect.value;
const quantity = parseFloat(quantityInput.value) || 0;
const priceValue = priceInput.value.trim();
const price = parseFloat(priceValue) || 0;
const sum = quantity * price;
// Обновляем дисплей суммы
sumDisplay.value = sum.toFixed(2);
// Только считаем если данные ПОЛНОСТЬЮ заполнены (включая цену!)
if (productId && quantity > 0 && priceValue !== '' && price >= 0) {
totalItems++;
totalQuantity += quantity;
totalSum += sum;
productsData.push({
product_id: parseInt(productId),
quantity: quantity,
cost_price: price
});
}
});
// Обновляем итоги
document.getElementById('totalItems').textContent = totalItems;
document.getElementById('totalQuantity').textContent = totalQuantity.toFixed(3);
document.getElementById('totalSum').textContent = totalSum.toFixed(2);
// Обновляем JSON данные
productsJsonInput.value = JSON.stringify(productsData);
// Отключаем кнопку отправки если нет товаров
submitBtn.disabled = totalItems === 0;
}
// Добавляем первую пустую строку
addProductRow();
// Восстанавливаем товары из JSON если была ошибка (сохранение данных при ошибке)
const savedProductsJson = '{{ products_json|escapejs }}';
if (savedProductsJson && savedProductsJson.trim() !== '[]' && savedProductsJson.trim() !== '') {
try {
const savedProducts = JSON.parse(savedProductsJson);
if (savedProducts && savedProducts.length > 0) {
// Удаляем пустую первую строку
productsBody.innerHTML = '';
rowCounter = 0;
// Добавляем восстановленные товары
savedProducts.forEach(item => {
addProductRow();
const lastRow = productsBody.querySelector('tr:last-child');
const lastSelect = lastRow.querySelector('.product-select');
// Создаём option и устанавливаем его в Select2
const newOption = new Option(item.product_name || `Товар #${item.product_id}`, item.product_id, true, true);
$(lastSelect).append(newOption).trigger('change');
lastRow.querySelector('.quantity-input').value = item.quantity;
lastRow.querySelector('.price-input').value = item.cost_price;
});
// Обновляем итоги
updateTotals();
// Очищаем поле номера документа для автогенерации
const documentNumberInput = document.querySelector('[name="document_number"]');
if (documentNumberInput) {
documentNumberInput.value = '';
}
}
} catch (e) {
console.error('Ошибка восстановления товаров:', e);
}
}
// Получаем элемент поля номера документа
const documentNumberInput = document.querySelector('[name="document_number"]');
// Валидация номера документа (запретить номера, начинающиеся с "IN-" только для заполненного поля)
documentNumberInput.addEventListener('change', function() {
const value = this.value.trim().toUpperCase();
const container = this.closest('.mb-3');
let errorDiv = container.querySelector('.document-number-error');
// Проверяем IN-* ТОЛЬКО если поле НЕ пусто
if (value && value.startsWith('IN-')) {
// Показать ошибку
this.classList.add('is-invalid');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'document-number-error text-danger small mt-2';
container.appendChild(errorDiv);
}
errorDiv.textContent = 'Номера, начинающиеся с "IN-", зарезервированы для системы. Если хотите автогенерацию, оставьте поле пустым.';
} else {
// Очистить ошибку (пусто или другой формат - это ОК)
this.classList.remove('is-invalid');
if (errorDiv) {
errorDiv.remove();
}
}
});
// Валидация перед отправкой
form.addEventListener('submit', function(e) {
// Проверка номера документа - запретить IN-* только если поле ЗАПОЛНЕНО
const docNumberValue = documentNumberInput.value.trim().toUpperCase();
const docNumberContainer = documentNumberInput.closest('.mb-3');
if (docNumberValue && docNumberValue.startsWith('IN-')) {
e.preventDefault();
documentNumberInput.classList.add('is-invalid');
let errorDiv = docNumberContainer.querySelector('.document-number-error');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'document-number-error text-danger small mt-2';
docNumberContainer.appendChild(errorDiv);
}
errorDiv.textContent = 'Номера, начинающиеся с "IN-", зарезервированы для системы. Оставьте пустым для автогенерации.';
alert('Номера, начинающиеся с "IN-", зарезервированы для системы. Оставьте пустым для автогенерации.');
documentNumberInput.focus();
return false;
}
// Проверка склада
const warehouseSelect = document.querySelector('[name="warehouse"]');
const warehouseContainer = warehouseSelect.closest('.mb-3');
if (!warehouseSelect.value) {
e.preventDefault();
// Добавляем класс ошибки если его нет
if (!warehouseSelect.classList.contains('is-invalid')) {
warehouseSelect.classList.add('is-invalid');
warehouseContainer.classList.add('has-validation');
}
// Создаём или обновляем сообщение об ошибке
let errorDiv = warehouseContainer.querySelector('.warehouse-error');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'warehouse-error text-danger small mt-2';
warehouseContainer.appendChild(errorDiv);
}
errorDiv.textContent = 'Пожалуйста, выберите склад перед отправкой.';
alert('Пожалуйста, выберите склад перед отправкой.');
warehouseSelect.focus();
return false;
} else {
// Очищаем ошибку если склад выбран
warehouseSelect.classList.remove('is-invalid');
const errorDiv = warehouseContainer.querySelector('.warehouse-error');
if (errorDiv) {
errorDiv.remove();
}
}
// СНАЧАЛА проверяем корректность заполнения товаров
let hasErrors = false;
let hasAnyProduct = false;
productsBody.querySelectorAll('tr').forEach(row => {
const productSelect = row.querySelector('.product-select');
const quantityInput = row.querySelector('.quantity-input');
const priceInput = row.querySelector('.price-input');
const productError = row.querySelector('td:nth-child(1) .error-message');
const quantityError = row.querySelector('td:nth-child(2) .error-message');
const priceError = row.querySelector('td:nth-child(3) .error-message');
let rowHasError = false;
// Проверка товара
if (!productSelect.value) {
productError.textContent = 'Выберите товар';
productError.style.display = 'block';
productSelect.classList.add('is-invalid');
hasErrors = true;
rowHasError = true;
} else {
productError.style.display = 'none';
productSelect.classList.remove('is-invalid');
hasAnyProduct = true;
}
// Проверка количества
const quantity = parseFloat(quantityInput.value) || 0;
if (quantity <= 0) {
quantityError.textContent = 'Количество должно быть > 0';
quantityError.style.display = 'block';
quantityInput.classList.add('is-invalid');
hasErrors = true;
rowHasError = true;
} else {
quantityError.style.display = 'none';
quantityInput.classList.remove('is-invalid');
}
// Проверка цены - ОБЯЗАТЕЛЬНОЕ ПОЛЕ!
const priceValue = priceInput.value.trim();
const price = parseFloat(priceValue);
if (priceValue === '' || isNaN(price)) {
priceError.textContent = 'Укажите цену закупки';
priceError.style.display = 'block';
priceInput.classList.add('is-invalid');
hasErrors = true;
rowHasError = true;
} else if (price < 0) {
priceError.textContent = 'Цена не может быть отрицательной';
priceError.style.display = 'block';
priceInput.classList.add('is-invalid');
hasErrors = true;
rowHasError = true;
} else {
priceError.style.display = 'none';
priceInput.classList.remove('is-invalid');
}
if (rowHasError) {
row.classList.add('row-error');
} else {
row.classList.remove('row-error');
}
});
// Проверка что есть хотя бы один товар
if (!hasAnyProduct) {
e.preventDefault();
alert('Пожалуйста, добавьте хотя бы один товар.');
return false;
}
// Если есть ошибки валидации - показываем
if (hasErrors) {
e.preventDefault();
alert('⚠️ Пожалуйста, исправьте ошибки в форме (см. подсвеченные поля).');
// Прокручиваем к первой ошибке
const firstError = productsBody.querySelector('.is-invalid');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstError.focus();
}
return false;
}
// Проверяем финальные данные
const productsData = JSON.parse(productsJsonInput.value);
if (productsData.length === 0) {
e.preventDefault();
alert('⚠️ Необходимо корректно заполнить все поля товаров.');
return false;
}
});
});
</script>
{% endblock %}

View File

@@ -1,50 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}Отмена приходу товара{% endblock %}
{% block breadcrumb_current %}Приходы{% endblock %}
{% block inventory_content %}
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<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>
<p class="text-muted">
Это действие удалит запись о приходе товара и может повлиять на остатки на складе.
</p>
<div class="alert alert-info">
<h5>Информация о приходе:</h5>
<ul class="mb-0">
<li><strong>Товар:</strong> {{ incoming.product.name }}</li>
<li><strong>Склад:</strong> {{ incoming.warehouse.name }}</li>
<li><strong>Количество:</strong> {{ incoming.quantity|smart_quantity }} шт</li>
<li><strong>Цена закупки:</strong> {{ incoming.cost_price }} руб.</li>
{% if incoming.document_number %}
<li><strong>Номер документа:</strong> {{ incoming.document_number }}</li>
{% endif %}
</ul>
</div>
<form method="post" class="mt-4">
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Подтвердить отмену
</button>
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Вернуться
</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,134 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}
{% if form.instance.pk %}
Редактирование прихода товара
{% else %}
Новый приход товара
{% endif %}
{% endblock %}
{% block breadcrumb_current %}Приходы{% endblock %}
{% block inventory_content %}
<div class="card">
<div class="card-header">
<h4 class="mb-0">
{% if form.instance.pk %}
Редактирование прихода
{% else %}
Регистрация нового поступления
{% endif %}
</h4>
</div>
<div class="card-body">
{% if form.instance.pk and not form.instance.can_edit %}
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<strong>Внимание!</strong> Этот приход товара уже обработан (для него создана складская партия).
Редактирование недоступно, так как партия может быть использована в продажах.
</div>
{% endif %}
<form method="post" class="form">
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.product.id_for_label }}" class="form-label">
{{ form.product.label }} <span class="text-danger">*</span>
</label>
{{ form.product }}
{% if form.product.errors %}
<div class="invalid-feedback d-block">
{% for error in form.product.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.warehouse.id_for_label }}" class="form-label">
{{ form.warehouse.label }} <span class="text-danger">*</span>
</label>
{{ form.warehouse }}
{% if form.warehouse.errors %}
<div class="invalid-feedback d-block">
{% for error in form.warehouse.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.quantity.id_for_label }}" class="form-label">
{{ form.quantity.label }} <span class="text-danger">*</span>
</label>
{{ form.quantity|smart_quantity }}
{% if form.quantity.errors %}
<div class="invalid-feedback d-block">
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.cost_price.id_for_label }}" class="form-label">
{{ form.cost_price.label }} <span class="text-danger">*</span>
</label>
{{ form.cost_price }}
{% if form.cost_price.errors %}
<div class="invalid-feedback d-block">
{% for error in form.cost_price.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<label for="{{ form.document_number.id_for_label }}" class="form-label">
{{ form.document_number.label }}
</label>
{{ form.document_number }}
{% if form.document_number.errors %}
<div class="invalid-feedback d-block">
{% for error in form.document_number.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.notes.id_for_label }}" class="form-label">
{{ form.notes.label }}
</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="invalid-feedback d-block">
{% for error in form.notes.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i>
{% if form.instance.pk %}
Сохранить
{% else %}
Создать
{% endif %}
</button>
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отменить
</a>
</div>
</form>
</div>
</div>
<style>
select, textarea, input {
width: 100%;
}
</style>
{% endblock %}

View File

@@ -1,126 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}История приходов товара{% endblock %}
{% block breadcrumb_current %}Приходы{% endblock %}
{% block inventory_content %}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">Приходы товара</h4>
<a href="{% url 'inventory:incoming-create' %}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle"></i> Новый приход
</a>
</div>
<div class="card-body">
{% if incomings %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Товар</th>
<th>Склад</th>
<th>Тип</th>
<th>Количество</th>
<th>Цена закупки</th>
<th>Номер документа</th>
<th>Партия</th>
<th>Дата</th>
<th class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for incoming in incomings %}
<tr>
<td><strong>{{ incoming.product.name }}</strong></td>
<td>{{ incoming.batch.warehouse.name }}</td>
<td>
<span class="badge bg-{% if incoming.batch.receipt_type == 'supplier' %}primary{% elif incoming.batch.receipt_type == 'inventory' %}info{% else %}success{% endif %}">
{{ incoming.batch.get_receipt_type_display }}
</span>
</td>
<td>{{ incoming.quantity|smart_quantity }} шт</td>
<td>{{ incoming.cost_price }} руб.</td>
<td>
{% if incoming.batch.document_number %}
<code>{{ incoming.batch.document_number }}</code>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if incoming.stock_batch %}
<a href="{% url 'inventory:batch-detail' incoming.stock_batch.pk %}" title="Перейти к партии на складе">
<strong>#{{ incoming.stock_batch.pk }}</strong>
</a>
{% else %}
<span class="badge bg-warning">Не назначена</span>
{% endif %}
</td>
<td>{{ incoming.created_at|date:"d.m.Y H:i" }}</td>
<td class="text-end">
{% if incoming.can_edit %}
<a href="{% url 'inventory:incoming-update' incoming.pk %}" class="btn btn-sm btn-outline-primary" title="Редактировать приход">
<i class="bi bi-pencil"></i>
</a>
{% else %}
<span class="btn btn-sm btn-outline-secondary disabled" title="Редактирование недоступно: приход уже обработан">
<i class="bi bi-pencil"></i>
</span>
{% endif %}
<a href="{% url 'inventory:incoming-delete' incoming.pk %}" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Приходов не найдено.
<a href="{% url 'inventory:incoming-create' %}" class="alert-link">Зарегистрировать новый приход</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -12,7 +12,7 @@
<!-- Breadcrumbs -->
<nav aria-label="breadcrumb" class="mb-2">
<ol class="breadcrumb breadcrumb-sm mb-0">
<li class="breadcrumb-item"><a href="{% url 'inventory:incoming-document-list' %}">Документы поступления</a></li>
<li class="breadcrumb-item"><a href="{% url 'inventory:incoming-list' %}">Документы поступления</a></li>
<li class="breadcrumb-item active">{{ document.document_number }}</li>
</ol>
</nav>
@@ -45,13 +45,13 @@
</h5>
{% if document.can_edit %}
<div class="btn-group">
<form method="post" action="{% url 'inventory:incoming-document-confirm' document.pk %}" class="d-inline">
<form method="post" action="{% url 'inventory:incoming-confirm' document.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-success btn-sm" {% if not document.can_confirm %}disabled{% endif %}>
<i class="bi bi-check-lg me-1"></i>Провести
</button>
</form>
<form method="post" action="{% url 'inventory:incoming-document-cancel' document.pk %}" class="d-inline ms-2">
<form method="post" action="{% url 'inventory:incoming-cancel' document.pk %}" class="d-inline ms-2">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Отменить документ?')">
<i class="bi bi-x-lg me-1"></i>Отменить
@@ -118,7 +118,7 @@
<div class="card-body">
<!-- Компонент поиска товаров -->
<div class="mb-3">
{% include 'products/components/product_search_picker.html' with container_id='incoming-document-picker' title='Найти товар для поступления' warehouse_id=document.warehouse.id filter_in_stock_only=False categories=categories tags=tags add_button_text='Выбрать товар' content_height='250px' %}
{% include 'products/components/product_search_picker.html' with container_id='incoming-picker' title='Найти товар для поступления' warehouse_id=document.warehouse.id filter_in_stock_only=False categories=categories tags=tags add_button_text='Выбрать товар' content_height='250px' %}
</div>
<!-- Информация о выбранном товаре -->
@@ -136,7 +136,7 @@
</div>
<!-- Форма добавления позиции -->
<form method="post" action="{% url 'inventory:incoming-document-add-item' document.pk %}" id="add-item-form">
<form method="post" action="{% url 'inventory:incoming-add-item' document.pk %}" id="add-item-form">
{% csrf_token %}
<div class="row g-3">
@@ -257,7 +257,7 @@
</button>
</div>
<form id="delete-form-{{ item.id }}" method="post"
action="{% url 'inventory:incoming-document-remove-item' document.pk item.pk %}"
action="{% url 'inventory:incoming-remove-item' document.pk item.pk %}"
style="display: none;">
{% csrf_token %}
</form>
@@ -308,7 +308,7 @@ document.addEventListener('DOMContentLoaded', function() {
const clearSelectedBtn = document.getElementById('clear-selected-product');
// Инициализация компонента поиска товаров
const picker = ProductSearchPicker.init('#incoming-document-picker', {
const picker = ProductSearchPicker.init('#incoming-picker', {
onAddSelected: function(product, instance) {
if (product) {
selectProduct(product);

View File

@@ -7,7 +7,7 @@
<!-- Breadcrumbs -->
<nav aria-label="breadcrumb" class="mb-2">
<ol class="breadcrumb breadcrumb-sm mb-0">
<li class="breadcrumb-item"><a href="{% url 'inventory:incoming-document-list' %}">Документы поступления</a></li>
<li class="breadcrumb-item"><a href="{% url 'inventory:incoming-list' %}">Документы поступления</a></li>
<li class="breadcrumb-item active">Создать</li>
</ol>
</nav>
@@ -66,7 +66,7 @@
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Создать
</button>
<a href="{% url 'inventory:incoming-document-list' %}" class="btn btn-outline-secondary">
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-outline-secondary">
Отмена
</a>
</div>

View File

@@ -9,7 +9,7 @@
<h4 class="mb-0">
<i class="bi bi-file-earmark-plus me-2"></i>Документы поступления
</h4>
<a href="{% url 'inventory:incoming-document-create' %}" class="btn btn-primary">
<a href="{% url 'inventory:incoming-create' %}" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>Создать документ
</a>
</div>
@@ -46,7 +46,7 @@
{% for doc in documents %}
<tr>
<td class="px-3 py-2">
<a href="{% url 'inventory:incoming-document-detail' doc.pk %}" class="fw-semibold text-decoration-none">
<a href="{% url 'inventory:incoming-detail' doc.pk %}" class="fw-semibold text-decoration-none">
{{ doc.document_number }}
</a>
</td>
@@ -70,7 +70,7 @@
{% if doc.created_by %}{{ doc.created_by.username }}{% else %}-{% endif %}
</td>
<td class="px-3 py-2 text-end">
<a href="{% url 'inventory:incoming-document-detail' doc.pk %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'inventory:incoming-detail' doc.pk %}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>

View File

@@ -84,7 +84,7 @@
{% if incoming_document %}
<p class="mb-1">
<strong>Документ оприходования:</strong>
<a href="{% url 'inventory:incoming-document-detail' incoming_document.pk %}" target="_blank">
<a href="{% url 'inventory:incoming-detail' incoming_document.pk %}" target="_blank">
{{ incoming_document.document_number }}
</a>
<span class="badge {% if incoming_document.status == 'confirmed' %}bg-success{% else %}bg-warning{% endif %} ms-2">