Files
octopus/myproject/inventory/views/writeoff_document.py
Andrey Smakotin 2e5ebabf22 Интегрирован компонент поиска товаров в документы списания с фильтром по складу
- Добавлен параметр warehouse в API search_products_and_variants
- API фильтрует товары по наличию на указанном складе через Stock
- Обновлен _apply_product_filters для поддержки warehouse_id
- ProductSearchPicker теперь поддерживает data-warehouse-id
- Warehouse автоматически передается в AJAX запросы
- В WriteOffDocumentDetailView добавлены categories и tags в контекст
- Компонент поиска встроен в detail.html с жестким фильтром по складу документа
- Single-select режим для выбора одного товара
- JS автоматически заполняет select формы при выборе товара
- Отображение выбранного товара с фото и артикулом
- Автофокус на поле количества после выбора товара
- Пользователь видит только товары доступные на складе документа
2025-12-11 00:02:37 +03:00

215 lines
8.5 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.
"""
Views для работы с документами списания (WriteOffDocument).
"""
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 WriteOffDocument, WriteOffDocumentItem
from inventory.forms import WriteOffDocumentForm, WriteOffDocumentItemForm
from inventory.services.writeoff_document_service import WriteOffDocumentService
class WriteOffDocumentListView(LoginRequiredMixin, ListView):
"""Список документов списания"""
model = WriteOffDocument
template_name = 'inventory/writeoff_document/list.html'
context_object_name = 'documents'
paginate_by = 20
def get_queryset(self):
return WriteOffDocument.objects.select_related(
'warehouse', 'created_by', 'confirmed_by'
).prefetch_related('items').order_by('-date', '-created_at')
class WriteOffDocumentCreateView(LoginRequiredMixin, CreateView):
"""Создание документа списания"""
model = WriteOffDocument
form_class = WriteOffDocumentForm
template_name = 'inventory/writeoff_document/form.html'
def form_valid(self, form):
document = WriteOffDocumentService.create_document(
warehouse=form.cleaned_data['warehouse'],
date=form.cleaned_data['date'],
notes=form.cleaned_data.get('notes'),
created_by=self.request.user
)
messages.success(self.request, f'Документ {document.document_number} создан')
return redirect('inventory:writeoff-document-detail', pk=document.pk)
class WriteOffDocumentDetailView(LoginRequiredMixin, DetailView):
"""Детальный просмотр документа списания"""
model = WriteOffDocument
template_name = 'inventory/writeoff_document/detail.html'
context_object_name = 'document'
def get_queryset(self):
return WriteOffDocument.objects.select_related(
'warehouse', 'created_by', 'confirmed_by'
).prefetch_related('items__product', 'items__reservation')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['item_form'] = WriteOffDocumentItemForm(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 WriteOffDocumentAddItemView(LoginRequiredMixin, View):
"""Добавление позиции в документ списания"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(WriteOffDocument, pk=pk)
form = WriteOffDocumentItemForm(request.POST, document=document)
if form.is_valid():
try:
item = WriteOffDocumentService.add_item(
document=document,
product=form.cleaned_data['product'],
quantity=form.cleaned_data['quantity'],
reason=form.cleaned_data['reason'],
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:writeoff-document-detail', pk=pk)
class WriteOffDocumentUpdateItemView(LoginRequiredMixin, View):
"""Обновление позиции документа списания"""
@transaction.atomic
def post(self, request, pk, item_pk):
document = get_object_or_404(WriteOffDocument, pk=pk)
item = get_object_or_404(WriteOffDocumentItem, pk=item_pk, document=document)
try:
quantity = request.POST.get('quantity')
reason = request.POST.get('reason')
notes = request.POST.get('notes')
WriteOffDocumentService.update_item(
item,
quantity=quantity,
reason=reason,
notes=notes
)
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:writeoff-document-detail', pk=pk)
class WriteOffDocumentRemoveItemView(LoginRequiredMixin, View):
"""Удаление позиции из документа списания"""
@transaction.atomic
def post(self, request, pk, item_pk):
document = get_object_or_404(WriteOffDocument, pk=pk)
item = get_object_or_404(WriteOffDocumentItem, pk=item_pk, document=document)
try:
product_name = item.product.name
WriteOffDocumentService.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:writeoff-document-detail', pk=pk)
class WriteOffDocumentConfirmView(LoginRequiredMixin, View):
"""Проведение документа списания"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(WriteOffDocument, pk=pk)
try:
result = WriteOffDocumentService.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:writeoff-document-detail', pk=pk)
class WriteOffDocumentCancelView(LoginRequiredMixin, View):
"""Отмена документа списания"""
@transaction.atomic
def post(self, request, pk):
document = get_object_or_404(WriteOffDocument, pk=pk)
try:
WriteOffDocumentService.cancel_document(document)
messages.success(request, f'Документ {document.document_number} отменён')
except ValidationError as e:
messages.error(request, str(e))
return redirect('inventory:writeoff-document-list')