Унификация генерации номеров документов и оптимизация кода
- Унифицирован формат номеров документов: IN-XXXXXX (6 цифр), как WO-XXXXXX и MOVE-XXXXXX - Убрано дублирование функции _extract_number_from_document_number - Оптимизирована инициализация счетчика incoming: быстрая проверка перед полной инициализацией - Удален неиспользуемый файл utils.py (функциональность перенесена в document_generator.py) - Все функции генерации номеров используют единый подход через DocumentCounter.get_next_value()
This commit is contained in:
@@ -125,6 +125,24 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Документы поступления -->
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<a href="{% url 'inventory:incoming-document-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">
|
||||
<i class="bi bi-file-earmark-plus text-success" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0 text-dark">Документы поступления</h6>
|
||||
<small class="text-muted">Коллективное поступление</small>
|
||||
</div>
|
||||
<i class="bi bi-chevron-right text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Перемещения -->
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<a href="{% url 'inventory:transfer-list' %}" class="card shadow-sm h-100 text-decoration-none">
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block title %}Документ поступления {{ document.document_number }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- CSS для компонента поиска -->
|
||||
<link rel="stylesheet" href="{% static 'products/css/product-search-picker.css' %}">
|
||||
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<!-- 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 active">{{ document.document_number }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Основной контент - одна колонка -->
|
||||
<div class="col-12">
|
||||
<!-- Информация о документе -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-light py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-plus me-2"></i>{{ document.document_number }}
|
||||
{% if document.status == 'draft' %}
|
||||
<span class="badge bg-warning text-dark ms-2">Черновик</span>
|
||||
{% elif document.status == 'confirmed' %}
|
||||
<span class="badge bg-success ms-2">Проведён</span>
|
||||
{% elif document.status == 'cancelled' %}
|
||||
<span class="badge bg-secondary ms-2">Отменён</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if document.can_edit %}
|
||||
<div class="btn-group">
|
||||
<form method="post" action="{% url 'inventory:incoming-document-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">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Отменить документ?')">
|
||||
<i class="bi bi-x-lg me-1"></i>Отменить
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<p class="text-muted small mb-1">Склад</p>
|
||||
<p class="fw-semibold">{{ document.warehouse.name }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-muted small mb-1">Дата документа</p>
|
||||
<p class="fw-semibold">{{ document.date|date:"d.m.Y" }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-muted small mb-1">Тип поступления</p>
|
||||
<p class="fw-semibold">{{ document.get_receipt_type_display }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-muted small mb-1">Создан</p>
|
||||
<p class="fw-semibold">{{ document.created_at|date:"d.m.Y H:i" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if document.supplier_name %}
|
||||
<div class="mb-3">
|
||||
<p class="text-muted small mb-1">Поставщик</p>
|
||||
<p class="fw-semibold">{{ document.supplier_name }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if document.notes %}
|
||||
<div class="mb-3">
|
||||
<p class="text-muted small mb-1">Примечания</p>
|
||||
<p>{{ document.notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if document.confirmed_at %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Проведён</p>
|
||||
<p class="fw-semibold">{{ document.confirmed_at|date:"d.m.Y H:i" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Провёл</p>
|
||||
<p class="fw-semibold">{% if document.confirmed_by %}{{ document.confirmed_by.username }}{% else %}-{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Добавление позиции -->
|
||||
{% if document.can_edit %}
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-square me-2"></i>Добавить позицию в документ</h6>
|
||||
</div>
|
||||
<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' %}
|
||||
</div>
|
||||
|
||||
<!-- Информация о выбранном товаре -->
|
||||
<div id="selected-product-info" class="alert alert-info mb-3" style="display: none;">
|
||||
<div class="d-flex align-items-center">
|
||||
<img id="selected-product-photo" src="" alt="" class="rounded me-2" style="width: 50px; height: 50px; object-fit: cover; display: none;">
|
||||
<div class="flex-grow-1">
|
||||
<strong id="selected-product-name" class="d-block"></strong>
|
||||
<small class="text-muted" id="selected-product-sku"></small>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selected-product" title="Очистить">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма добавления позиции -->
|
||||
<form method="post" action="{% url 'inventory:incoming-document-add-item' document.pk %}" id="add-item-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label for="id_product" class="form-label">Товар <span class="text-danger">*</span></label>
|
||||
{{ item_form.product }}
|
||||
{% if item_form.product.errors %}
|
||||
<div class="text-danger small">{{ item_form.product.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="id_quantity" class="form-label">Количество <span class="text-danger">*</span></label>
|
||||
{{ item_form.quantity }}
|
||||
{% if item_form.quantity.errors %}
|
||||
<div class="text-danger small">{{ item_form.quantity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="id_cost_price" class="form-label">Закупочная цена <span class="text-danger">*</span></label>
|
||||
{{ item_form.cost_price }}
|
||||
{% if item_form.cost_price.errors %}
|
||||
<div class="text-danger small">{{ item_form.cost_price.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<label for="id_notes" class="form-label">Примечания</label>
|
||||
{{ item_form.notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle me-1"></i>Добавить в документ
|
||||
</button>
|
||||
<small class="text-muted ms-3">
|
||||
<i class="bi bi-info-circle"></i> Используйте поиск выше для быстрого выбора товара
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Таблица позиций -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h6 class="mb-0"><i class="bi bi-table me-2"></i>Позиции документа</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2">Товар</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>
|
||||
{% if document.can_edit %}
|
||||
<th scope="col" class="px-3 py-2" style="width: 100px;"></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in document.items.all %}
|
||||
<tr id="item-row-{{ item.id }}" data-item-id="{{ item.id }}">
|
||||
<td class="px-3 py-2">
|
||||
<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;">
|
||||
{% 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"
|
||||
style="display: none; width: 100px; text-align: right; margin-left: auto;">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-end" style="width: 120px;">
|
||||
<strong>{{ item.total_cost|floatformat:2 }}</strong>
|
||||
</td>
|
||||
<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="Примечания"
|
||||
style="display: none;">
|
||||
{% 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">
|
||||
<button type="button" class="btn btn-outline-primary btn-edit-item" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
onclick="if(confirm('Удалить позицию?')) document.getElementById('delete-form-{{ item.id }}').submit();"
|
||||
title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm item-edit-buttons" style="display: none;">
|
||||
<button type="button" class="btn btn-success btn-save-item" title="Сохранить">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-cancel-edit" title="Отменить">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form id="delete-form-{{ item.id }}" method="post"
|
||||
action="{% url 'inventory:incoming-document-remove-item' document.pk item.pk %}"
|
||||
style="display: none;">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="{% if document.can_edit %}6{% else %}5{% endif %}" class="px-3 py-4 text-center text-muted">
|
||||
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
|
||||
Позиций пока нет
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if document.items.exists %}
|
||||
<tfoot class="table-light">
|
||||
<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="{% if document.can_edit %}2{% else %}1{% endif %}"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS для компонента поиска -->
|
||||
<script src="{% static 'products/js/product-search-picker.js' %}?v=3"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Элементы формы
|
||||
const productSelect = document.querySelector('#id_product');
|
||||
const quantityInput = document.querySelector('#id_quantity');
|
||||
const costPriceInput = document.querySelector('#id_cost_price');
|
||||
|
||||
// Элементы отображения выбранного товара
|
||||
const selectedProductInfo = document.getElementById('selected-product-info');
|
||||
const selectedProductName = document.getElementById('selected-product-name');
|
||||
const selectedProductSku = document.getElementById('selected-product-sku');
|
||||
const selectedProductPhoto = document.getElementById('selected-product-photo');
|
||||
const clearSelectedBtn = document.getElementById('clear-selected-product');
|
||||
|
||||
// Инициализация компонента поиска товаров
|
||||
const picker = ProductSearchPicker.init('#incoming-document-picker', {
|
||||
onAddSelected: function(product, instance) {
|
||||
if (product) {
|
||||
selectProduct(product);
|
||||
instance.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция выбора товара
|
||||
function selectProduct(product) {
|
||||
const productId = String(product.id).replace('product_', '');
|
||||
const productName = product.text || product.name || '';
|
||||
|
||||
// Показываем информацию о выбранном товаре
|
||||
selectedProductName.textContent = productName;
|
||||
selectedProductSku.textContent = product.sku || '';
|
||||
|
||||
if (product.photo_url) {
|
||||
selectedProductPhoto.src = product.photo_url;
|
||||
selectedProductPhoto.style.display = 'block';
|
||||
} else {
|
||||
selectedProductPhoto.style.display = 'none';
|
||||
}
|
||||
|
||||
selectedProductInfo.style.display = 'block';
|
||||
|
||||
// Устанавливаем значение в select формы
|
||||
if (productSelect) {
|
||||
productSelect.value = productId;
|
||||
// Триггерим change для Select2, если он используется
|
||||
const event = new Event('change', { bubbles: true });
|
||||
productSelect.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Фокус в поле количества
|
||||
if (quantityInput) {
|
||||
quantityInput.focus();
|
||||
quantityInput.select();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция очистки выбора товара
|
||||
function clearSelectedProduct() {
|
||||
selectedProductInfo.style.display = 'none';
|
||||
selectedProductName.textContent = '';
|
||||
selectedProductSku.textContent = '';
|
||||
selectedProductPhoto.style.display = 'none';
|
||||
|
||||
if (productSelect) {
|
||||
productSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка выбора товара
|
||||
if (clearSelectedBtn) {
|
||||
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Inline редактирование позиций в таблице
|
||||
// ============================================
|
||||
|
||||
// Хранилище оригинальных значений при редактировании
|
||||
const originalValues = {};
|
||||
|
||||
// Обработчики для кнопок редактирования
|
||||
document.querySelectorAll('.btn-edit-item').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const row = this.closest('tr');
|
||||
const itemId = row.dataset.itemId;
|
||||
|
||||
// Сохраняем оригинальные значения
|
||||
originalValues[itemId] = {
|
||||
quantity: row.querySelector('.item-quantity-input').value,
|
||||
cost_price: row.querySelector('.item-cost-price-input').value,
|
||||
notes: row.querySelector('.item-notes-input').value
|
||||
};
|
||||
|
||||
// Переключаем в режим редактирования
|
||||
toggleEditMode(row, true);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчики для кнопок сохранения
|
||||
document.querySelectorAll('.btn-save-item').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const row = this.closest('tr');
|
||||
const itemId = row.dataset.itemId;
|
||||
saveItemChanges(itemId, row);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчики для кнопок отмены
|
||||
document.querySelectorAll('.btn-cancel-edit').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const row = this.closest('tr');
|
||||
const itemId = row.dataset.itemId;
|
||||
|
||||
// Восстанавливаем оригинальные значения
|
||||
if (originalValues[itemId]) {
|
||||
row.querySelector('.item-quantity-input').value = originalValues[itemId].quantity;
|
||||
row.querySelector('.item-cost-price-input').value = originalValues[itemId].cost_price;
|
||||
row.querySelector('.item-notes-input').value = originalValues[itemId].notes;
|
||||
}
|
||||
|
||||
// Выходим из режима редактирования
|
||||
toggleEditMode(row, false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Переключение режима редактирования строки
|
||||
*/
|
||||
function toggleEditMode(row, isEditing) {
|
||||
// Переключаем видимость полей отображения/ввода
|
||||
row.querySelectorAll('.item-quantity-display, .item-cost-price-display, .item-notes-display').forEach(el => {
|
||||
el.style.display = isEditing ? 'none' : '';
|
||||
});
|
||||
row.querySelectorAll('.item-quantity-input, .item-cost-price-input, .item-notes-input').forEach(el => {
|
||||
el.style.display = isEditing ? '' : 'none';
|
||||
});
|
||||
|
||||
// Переключаем видимость кнопок
|
||||
row.querySelector('.item-action-buttons').style.display = isEditing ? 'none' : '';
|
||||
row.querySelector('.item-edit-buttons').style.display = isEditing ? '' : 'none';
|
||||
|
||||
// Фокус на поле количества при входе в режим редактирования
|
||||
if (isEditing) {
|
||||
const qtyInput = row.querySelector('.item-quantity-input');
|
||||
if (qtyInput) {
|
||||
qtyInput.focus();
|
||||
qtyInput.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение изменений позиции
|
||||
*/
|
||||
function saveItemChanges(itemId, row) {
|
||||
const quantity = row.querySelector('.item-quantity-input').value;
|
||||
const costPrice = row.querySelector('.item-cost-price-input').value;
|
||||
const notes = row.querySelector('.item-notes-input').value;
|
||||
|
||||
// Валидация
|
||||
if (!quantity || parseFloat(quantity) <= 0) {
|
||||
alert('Количество должно быть больше нуля');
|
||||
return;
|
||||
}
|
||||
if (!costPrice || parseFloat(costPrice) < 0) {
|
||||
alert('Закупочная цена не может быть отрицательной');
|
||||
return;
|
||||
}
|
||||
|
||||
// Отправляем на сервер
|
||||
const formData = new FormData();
|
||||
formData.append('quantity', quantity);
|
||||
formData.append('cost_price', costPrice);
|
||||
formData.append('notes', notes);
|
||||
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
|
||||
|
||||
// Блокируем кнопки во время сохранения
|
||||
const saveBtn = row.querySelector('.btn-save-item');
|
||||
const cancelBtn = row.querySelector('.btn-cancel-edit');
|
||||
saveBtn.disabled = true;
|
||||
cancelBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
|
||||
|
||||
fetch(`/inventory/incoming-documents/{{ document.pk }}/update-item/${itemId}/`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Обновляем отображение
|
||||
let formattedQty = parseFloat(quantity);
|
||||
if (formattedQty === Math.floor(formattedQty)) {
|
||||
formattedQty = Math.floor(formattedQty).toString();
|
||||
} else {
|
||||
formattedQty = formattedQty.toString().replace('.', ',');
|
||||
}
|
||||
row.querySelector('.item-quantity-display').textContent = formattedQty;
|
||||
row.querySelector('.item-cost-price-display').textContent = parseFloat(costPrice).toFixed(2);
|
||||
row.querySelector('.item-notes-display').textContent = notes || '-';
|
||||
|
||||
// Пересчитываем сумму
|
||||
const totalCost = (parseFloat(quantity) * parseFloat(costPrice)).toFixed(2);
|
||||
row.querySelector('td:nth-child(4) strong').textContent = totalCost;
|
||||
|
||||
// Выходим из режима редактирования
|
||||
toggleEditMode(row, false);
|
||||
} else {
|
||||
alert('Ошибка: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при сохранении');
|
||||
})
|
||||
.finally(() => {
|
||||
saveBtn.disabled = false;
|
||||
cancelBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Создать документ поступления{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<!-- 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 active">Создать</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-plus me-2"></i>Новый документ поступления
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_warehouse" class="form-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 class="mb-3">
|
||||
<label for="id_date" class="form-label">Дата документа <span class="text-danger">*</span></label>
|
||||
{{ form.date }}
|
||||
{% if form.date.errors %}
|
||||
<div class="text-danger small">{{ form.date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_receipt_type" class="form-label">Тип поступления <span class="text-danger">*</span></label>
|
||||
{{ form.receipt_type }}
|
||||
{% if form.receipt_type.errors %}
|
||||
<div class="text-danger small">{{ form.receipt_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="supplier-name-group">
|
||||
<label for="id_supplier_name" class="form-label">Наименование поставщика</label>
|
||||
{{ form.supplier_name }}
|
||||
{% if form.supplier_name.errors %}
|
||||
<div class="text-danger small">{{ form.supplier_name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="form-text text-muted">Заполняется для типа "Поступление от поставщика"</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_notes" class="form-label">Примечания</label>
|
||||
{{ form.notes }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const receiptTypeSelect = document.getElementById('id_receipt_type');
|
||||
const supplierNameGroup = document.getElementById('supplier-name-group');
|
||||
const supplierNameInput = document.getElementById('id_supplier_name');
|
||||
|
||||
function toggleSupplierName() {
|
||||
if (receiptTypeSelect.value === 'supplier') {
|
||||
supplierNameGroup.style.display = 'block';
|
||||
supplierNameInput.required = true;
|
||||
} else {
|
||||
supplierNameGroup.style.display = 'none';
|
||||
supplierNameInput.required = false;
|
||||
supplierNameInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
receiptTypeSelect.addEventListener('change', toggleSupplierName);
|
||||
toggleSupplierName(); // Инициализация при загрузке
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Документы поступления{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<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">
|
||||
<i class="bi bi-plus-lg me-1"></i>Создать документ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<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">Склад</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">Позиций</th>
|
||||
<th scope="col" class="px-3 py-2 text-end">Количество</th>
|
||||
<th scope="col" class="px-3 py-2">Создал</th>
|
||||
<th scope="col" class="px-3 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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">
|
||||
{{ doc.document_number }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ doc.date|date:"d.m.Y" }}</td>
|
||||
<td class="px-3 py-2">{{ doc.warehouse.name }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="badge bg-info">{{ doc.get_receipt_type_display }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
{% if doc.status == 'draft' %}
|
||||
<span class="badge bg-warning text-dark">Черновик</span>
|
||||
{% elif doc.status == 'confirmed' %}
|
||||
<span class="badge bg-success">Проведён</span>
|
||||
{% elif doc.status == 'cancelled' %}
|
||||
<span class="badge bg-secondary">Отменён</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-end">{{ doc.items.count }}</td>
|
||||
<td class="px-3 py-2 text-end">{{ doc.total_quantity }}</td>
|
||||
<td class="px-3 py-2">
|
||||
{% 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">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="px-3 py-4 text-center text-muted">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||
Документов поступления пока нет
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav class="mt-3">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Назад</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">{{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Вперёд</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user