Унификация генерации номеров документов и оптимизация кода

- Унифицирован формат номеров документов: 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:
2025-12-21 00:51:08 +03:00
parent 78dc9e9801
commit 375ec5366a
14 changed files with 1873 additions and 147 deletions

View File

@@ -27,6 +27,16 @@ from .inventory_ops import (
InventoryLineCreateBulkView
)
from .writeoff import WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView
from .writeoff_document import (
WriteOffDocumentListView, WriteOffDocumentCreateView, WriteOffDocumentDetailView,
WriteOffDocumentAddItemView, WriteOffDocumentUpdateItemView, WriteOffDocumentRemoveItemView,
WriteOffDocumentConfirmView, WriteOffDocumentCancelView
)
from .incoming_document import (
IncomingDocumentListView, IncomingDocumentCreateView, IncomingDocumentDetailView,
IncomingDocumentAddItemView, IncomingDocumentUpdateItemView, IncomingDocumentRemoveItemView,
IncomingDocumentConfirmView, IncomingDocumentCancelView
)
from .transfer import TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView
from .reservation import ReservationListView
from .stock import StockListView, StockDetailView
@@ -57,6 +67,14 @@ __all__ = [
'InventoryListView', 'InventoryCreateView', 'InventoryDetailView', 'InventoryLineCreateBulkView',
# WriteOff
'WriteOffListView', 'WriteOffCreateView', 'WriteOffUpdateView', 'WriteOffDeleteView',
# WriteOffDocument
'WriteOffDocumentListView', 'WriteOffDocumentCreateView', 'WriteOffDocumentDetailView',
'WriteOffDocumentAddItemView', 'WriteOffDocumentUpdateItemView', 'WriteOffDocumentRemoveItemView',
'WriteOffDocumentConfirmView', 'WriteOffDocumentCancelView',
# IncomingDocument
'IncomingDocumentListView', 'IncomingDocumentCreateView', 'IncomingDocumentDetailView',
'IncomingDocumentAddItemView', 'IncomingDocumentUpdateItemView', 'IncomingDocumentRemoveItemView',
'IncomingDocumentConfirmView', 'IncomingDocumentCancelView',
# Transfer
'TransferListView', 'TransferBulkCreateView', 'TransferDetailView', 'TransferDeleteView', 'GetProductStockView',
# Reservation

View File

@@ -0,0 +1,217 @@
"""
Views для работы с документами поступления (IncomingDocument).
"""
from django.views.generic import ListView, CreateView, DetailView, View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction
from django.core.exceptions import ValidationError
from inventory.models import IncomingDocument, IncomingDocumentItem
from inventory.forms import IncomingDocumentForm, IncomingDocumentItemForm
from inventory.services.incoming_document_service import IncomingDocumentService
class IncomingDocumentListView(LoginRequiredMixin, ListView):
"""Список документов поступления"""
model = IncomingDocument
template_name = 'inventory/incoming_document/incoming_document_list.html'
context_object_name = 'documents'
paginate_by = 20
def get_queryset(self):
return IncomingDocument.objects.select_related(
'warehouse', 'created_by', 'confirmed_by'
).prefetch_related('items').order_by('-date', '-created_at')
class IncomingDocumentCreateView(LoginRequiredMixin, CreateView):
"""Создание документа поступления"""
model = IncomingDocument
form_class = IncomingDocumentForm
template_name = 'inventory/incoming_document/incoming_document_form.html'
def form_valid(self, form):
document = IncomingDocumentService.create_document(
warehouse=form.cleaned_data['warehouse'],
date=form.cleaned_data['date'],
receipt_type=form.cleaned_data['receipt_type'],
supplier_name=form.cleaned_data.get('supplier_name'),
notes=form.cleaned_data.get('notes'),
created_by=self.request.user
)
messages.success(self.request, f'Документ {document.document_number} создан')
return redirect('inventory:incoming-document-detail', pk=document.pk)
class IncomingDocumentDetailView(LoginRequiredMixin, DetailView):
"""Детальный просмотр документа поступления"""
model = IncomingDocument
template_name = 'inventory/incoming_document/incoming_document_detail.html'
context_object_name = 'document'
def get_queryset(self):
return IncomingDocument.objects.select_related(
'warehouse', 'created_by', 'confirmed_by'
).prefetch_related('items__product')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['item_form'] = IncomingDocumentItemForm(document=self.object)
# Добавляем категории и теги для компонента поиска товаров
from products.models import ProductCategory, ProductTag
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
return context
class IncomingDocumentAddItemView(LoginRequiredMixin, View):
"""Добавление позиции в документ поступления"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(IncomingDocument, pk=pk)
form = IncomingDocumentItemForm(request.POST, document=document)
if form.is_valid():
try:
item = IncomingDocumentService.add_item(
document=document,
product=form.cleaned_data['product'],
quantity=form.cleaned_data['quantity'],
cost_price=form.cleaned_data['cost_price'],
notes=form.cleaned_data.get('notes')
)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'item_id': item.id,
'message': f'Добавлено: {item.product.name}'
})
messages.success(request, f'Добавлено: {item.product.name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
errors = '; '.join([f"{k}: {v[0]}" for k, v in form.errors.items()])
return JsonResponse({'success': False, 'error': errors}, status=400)
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
return redirect('inventory:incoming-document-detail', pk=pk)
class IncomingDocumentUpdateItemView(LoginRequiredMixin, View):
"""Обновление позиции документа поступления"""
@transaction.atomic
def post(self, request, pk, item_pk):
document = get_object_or_404(IncomingDocument, pk=pk)
item = get_object_or_404(IncomingDocumentItem, pk=item_pk, document=document)
try:
quantity = request.POST.get('quantity')
cost_price = request.POST.get('cost_price')
notes = request.POST.get('notes')
IncomingDocumentService.update_item(
item,
quantity=quantity if quantity else None,
cost_price=cost_price if cost_price else None,
notes=notes if notes else None
)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': f'Обновлено: {item.product.name}'
})
messages.success(request, f'Обновлено: {item.product.name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
return redirect('inventory:incoming-document-detail', pk=pk)
class IncomingDocumentRemoveItemView(LoginRequiredMixin, View):
"""Удаление позиции из документа поступления"""
@transaction.atomic
def post(self, request, pk, item_pk):
document = get_object_or_404(IncomingDocument, pk=pk)
item = get_object_or_404(IncomingDocumentItem, pk=item_pk, document=document)
try:
product_name = item.product.name
IncomingDocumentService.remove_item(item)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': f'Удалено: {product_name}'
})
messages.success(request, f'Удалено: {product_name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
return redirect('inventory:incoming-document-detail', pk=pk)
class IncomingDocumentConfirmView(LoginRequiredMixin, View):
"""Проведение документа поступления"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(IncomingDocument, pk=pk)
try:
result = IncomingDocumentService.confirm_document(
document,
confirmed_by=request.user
)
messages.success(
request,
f'Документ {document.document_number} проведён. '
f'Оприходовано {result["total_quantity"]} шт на сумму {result["total_cost"]:.2f}'
)
except ValidationError as e:
messages.error(request, str(e))
return redirect('inventory:incoming-document-detail', pk=pk)
class IncomingDocumentCancelView(LoginRequiredMixin, View):
"""Отмена документа поступления"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(IncomingDocument, pk=pk)
try:
IncomingDocumentService.cancel_document(document)
messages.success(request, f'Документ {document.document_number} отменён')
except ValidationError as e:
messages.error(request, str(e))
return redirect('inventory:incoming-document-list')