Добавлено разделение типов поступлений на склад

- Добавлено поле receipt_type в модель IncomingBatch с типами: supplier, inventory, adjustment
- Исправлен баг в InventoryProcessor: теперь корректно создается IncomingBatch при инвентаризации
- Создан IncomingAdjustmentCreateView для оприходования без инвентаризации
- Обновлены формы, шаблоны и админка для поддержки разных типов поступлений
- Добавлена навигация и URL для оприходования
- Тип поступления отображается в списках приходов и партий
This commit is contained in:
2025-12-20 23:47:13 +03:00
parent f1798291e0
commit 78dc9e9801
12 changed files with 238 additions and 20 deletions

View File

@@ -119,6 +119,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
# Оставляем пустой document_number как есть - модель будет генерировать при сохранении
document_number = form.cleaned_data.get('document_number', '').strip() or None
receipt_type = form.cleaned_data.get('receipt_type', 'supplier')
supplier_name = form.cleaned_data.get('supplier_name', '')
header_notes = form.cleaned_data.get('notes', '')
@@ -148,7 +149,8 @@ class IncomingCreateView(LoginRequiredMixin, View):
batch = IncomingBatch.objects.create(
warehouse=warehouse,
document_number=document_number,
supplier_name=supplier_name,
receipt_type=receipt_type,
supplier_name=supplier_name if receipt_type == 'supplier' else '',
notes=header_notes
)
file_logger.info(f" ✓ Created batch: {document_number}")
@@ -208,7 +210,8 @@ class IncomingCreateView(LoginRequiredMixin, View):
}
return render(request, self.template_name, context)
def _parse_products_from_post(self, post_data):
@staticmethod
def _parse_products_from_post(post_data):
"""
Парсит данные товаров из POST данных.
Ожидается formato:
@@ -235,3 +238,139 @@ class IncomingCreateView(LoginRequiredMixin, View):
pass
return products_data
class IncomingAdjustmentCreateView(LoginRequiredMixin, View):
"""
Создание оприходования товара на склад (без инвентаризации).
Аналогично IncomingCreateView, но с типом 'adjustment' и без поля поставщика.
"""
template_name = 'inventory/incoming/incoming_bulk_form.html'
def get(self, request):
"""Отображение формы ввода товаров."""
form = IncomingForm(initial={'receipt_type': 'adjustment'})
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(status='active').order_by('name')
# Генерируем номер документа автоматически
generated_document_number = generate_incoming_document_number()
context = {
'form': form,
'products': products,
'generated_document_number': generated_document_number,
'is_adjustment': True, # Флаг для шаблона, чтобы скрыть supplier_name
}
return render(request, self.template_name, context)
def post(self, request):
"""Обработка формы ввода товаров."""
# Устанавливаем receipt_type в 'adjustment'
post_data = request.POST.copy()
post_data['receipt_type'] = 'adjustment'
form = IncomingForm(post_data)
if not form.is_valid():
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'errors': form.errors,
'is_adjustment': True,
}
return render(request, self.template_name, context)
# Получаем данные header
warehouse = form.cleaned_data['warehouse']
document_number = form.cleaned_data.get('document_number', '').strip() or None
receipt_type = 'adjustment' # Всегда adjustment для этого view
header_notes = form.cleaned_data.get('notes', '')
# Получаем данные товаров из POST
products_data = IncomingCreateView._parse_products_from_post(request.POST)
if not products_data:
messages.error(request, 'Пожалуйста, добавьте хотя бы один товар.')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'is_adjustment': True,
}
return render(request, self.template_name, context)
# Генерируем номер партии один раз (если не указан)
if not document_number:
document_number = generate_incoming_document_number()
file_logger.info(f"--- POST started (adjustment) | batch_doc_number={document_number} | items_count={len(products_data)}")
try:
# Используем транзакцию для атомарности: либо все товары, либо ничего
with transaction.atomic():
# 1. Создаем партию (содержит номер документа и метаданные)
batch = IncomingBatch.objects.create(
warehouse=warehouse,
document_number=document_number,
receipt_type=receipt_type,
supplier_name='', # Не заполняем для adjustment
notes=header_notes
)
file_logger.info(f" ✓ Created batch: {document_number}")
# 2. Создаем товары в этой партии
created_count = 0
for product_data in products_data:
incoming = Incoming.objects.create(
batch=batch,
product_id=product_data['product_id'],
quantity=product_data['quantity'],
cost_price=product_data['cost_price'],
)
created_count += 1
file_logger.info(f" ✓ Item: {incoming.product.name} ({incoming.quantity} шт)")
file_logger.info(f"✓ SUCCESS: Batch {document_number} with {created_count} items")
messages.success(
request,
f'✓ Успешно создано оприходование "{document_number}" с {created_count} товарами.'
)
return redirect('inventory:incoming-list')
except IntegrityError as e:
file_logger.error(f"✗ IntegrityError: {str(e)} | doc_number={document_number}")
if 'document_number' in str(e):
error_msg = (
f'❌ Номер документа "{document_number}" уже существует в системе. '
f'Пожалуйста, оставьте поле пустым для автогенерации свободного номера. '
f'Данные, которые вы вводили, сохранены ниже.'
)
messages.error(request, error_msg)
else:
messages.error(request, f'Ошибка при создании оприходования: {str(e)}')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'products_json': request.POST.get('products_json', '[]'),
'is_adjustment': True,
}
return render(request, self.template_name, context)
except Exception as e:
messages.error(
request,
f'❌ Ошибка при создании оприходования: {str(e)}'
)
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'products_json': request.POST.get('products_json', '[]'),
'is_adjustment': True,
}
return render(request, self.template_name, context)