Files
octopus/myproject/inventory/views/incoming.py
Andrey Smakotin 131d078ac4 Запрет редактирования приходов после создания складской партии
- Добавлено свойство can_edit в модель Incoming
- Добавлена проверка в IncomingUpdateView для запрета редактирования обработанных приходов
- Скрыта кнопка редактирования в списке приходов для обработанных записей
- Добавлено предупреждение в форму редактирования

Это предотвращает проблемы с целостностью данных при FIFO-списаниях, когда партия уже может быть использована в продажах.
2025-12-25 23:01:12 +03:00

390 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
import logging
from django.shortcuts import render, redirect
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, View
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.http import JsonResponse, Http404
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from django.db import IntegrityError, transaction
from ..models import Incoming, IncomingBatch, Warehouse
from ..forms import IncomingForm, IncomingLineForm, IncomingModelForm
from inventory.utils import generate_incoming_document_number
from products.models import Product
file_logger = logging.getLogger('incoming_sequence_file')
class IncomingListView(LoginRequiredMixin, ListView):
"""
Список всех приходов товара (истории поступлений)
"""
model = Incoming
template_name = 'inventory/incoming/incoming_list.html'
context_object_name = 'incomings'
paginate_by = 20
def get_queryset(self):
queryset = Incoming.objects.select_related('product', 'batch', 'batch__warehouse').order_by('-created_at')
# Фильтры (если переданы)
product_id = self.request.GET.get('product')
warehouse_id = self.request.GET.get('warehouse')
if product_id:
queryset = queryset.filter(product_id=product_id)
if warehouse_id:
queryset = queryset.filter(batch__warehouse_id=warehouse_id)
return queryset
class IncomingUpdateView(LoginRequiredMixin, UpdateView):
"""
Редактирование поступления (только если ещё не обработано).
Обработанные приходы редактировать нельзя.
"""
model = Incoming
form_class = IncomingModelForm
template_name = 'inventory/incoming/incoming_form.html'
success_url = reverse_lazy('inventory:incoming-list')
def dispatch(self, request, *args, **kwargs):
"""Проверяем, можно ли редактировать приход"""
obj = self.get_object()
if not obj.can_edit:
messages.error(
request,
f'Нельзя редактировать приход товара "{obj.product.name}", '
f'так как для него уже создана складская партия. '
f'Редактирование возможно только до обработки прихода.'
)
return redirect('inventory:incoming-list')
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
messages.success(self.request, f'Приход товара обновлён.')
return super().form_valid(form)
class IncomingDeleteView(LoginRequiredMixin, DeleteView):
"""
Отмена/удаление поступления товара.
"""
model = Incoming
template_name = 'inventory/incoming/incoming_confirm_delete.html'
success_url = reverse_lazy('inventory:incoming-list')
def form_valid(self, form):
incoming = self.get_object()
messages.success(
self.request,
f'Приход товара "{incoming.product.name}" отменён.'
)
return super().form_valid(form)
class IncomingCreateView(LoginRequiredMixin, View):
"""
Создание поступлений товара на склад.
Позволяет добавить один или несколько товаров в одной форме
с одинаковым номером документа и складом.
По умолчанию показывается одна пустая строка товара,
но пользователь может добавить неограниченное количество товаров.
"""
template_name = 'inventory/incoming/incoming_bulk_form.html'
def get(self, request):
"""Отображение формы ввода товаров."""
form = IncomingForm()
# 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,
}
return render(request, self.template_name, context)
def post(self, request):
"""Обработка формы ввода товаров."""
form = IncomingForm(request.POST)
if not form.is_valid():
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'errors': form.errors,
}
return render(request, self.template_name, context)
# Получаем данные header
warehouse = form.cleaned_data['warehouse']
# Оставляем пустой 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', '')
# Получаем данные товаров из POST
products_data = self._parse_products_from_post(request.POST)
if not products_data:
messages.error(request, 'Пожалуйста, добавьте хотя бы один товар.')
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
}
return render(request, self.template_name, context)
# Генерируем номер партии один раз (если не указан)
if not document_number:
document_number = generate_incoming_document_number()
file_logger.info(f"--- POST started | 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=supplier_name if receipt_type == 'supplier' else '',
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', '[]'),
}
return render(request, self.template_name, context)
except Exception as e:
messages.error(
request,
f'❌ Ошибка при создании приходов: {str(e)}'
)
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
'products_json': request.POST.get('products_json', '[]'), # Сохраняем товары
}
return render(request, self.template_name, context)
@staticmethod
def _parse_products_from_post(post_data):
"""
Парсит данные товаров из POST данных.
Ожидается formato:
product_ids: [1, 2, 3]
quantities: [100, 50, 30]
cost_prices: [50, 30, 20]
"""
products_data = []
# Получаем JSON данные из hidden input (если используется)
import json
products_json = post_data.get('products_json', '[]')
try:
products_list = json.loads(products_json)
for item in products_list:
if item.get('product_id') and item.get('quantity') and item.get('cost_price'):
products_data.append({
'product_id': int(item['product_id']),
'quantity': float(item['quantity']),
'cost_price': float(item['cost_price']),
})
except (json.JSONDecodeError, ValueError):
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)