feat: Реализовать систему поступления товаров с партиями (IncomingBatch)

Основные изменения:
- Создана модель IncomingBatch для группировки товаров по документам
- Каждое поступление (Incoming) связано с одной батчем поступления
- Автоматическое создание StockBatch для каждого товара в приходе
- Реализована система нумерации партий (IN-XXXX-XXXX) с поиском максимума в БД
- Обновлены все представления (views) для работы с новой архитектурой
- Добавлены детальные страницы просмотра партий поступлений
- Обновлены шаблоны для отображения информации о партиях и их товарах
- Исправлена логика сигналов для создания StockBatch при приходе товара
- Обновлены формы для работы с новой структурой IncomingBatch

Архитектура FIFO:
- IncomingBatch: одна партия поступления (номер IN-XXXX-XXXX)
- Incoming: товар в партии поступления
- StockBatch: одна партия товара на складе (создается для каждого товара)

Это позволяет системе правильно применять FIFO при продаже товаров.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-29 03:26:06 +03:00
parent 097d4ea304
commit 6735be9b08
73 changed files with 6536 additions and 122 deletions

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""
Inventory Views Package
Организация views по модулям:
- warehouse.py: Управление складами
- incoming.py: Управление приходами товара
- sale.py: Управление продажами
- inventory_ops.py: Инвентаризация и её строки
- writeoff.py: Списания товара
- transfer.py: Перемещения между складами
- reservation.py: Резервирования товара
- stock.py: Справочник остатков (view-only)
- batch.py: Справочник партий товара (view-only)
- allocation.py: Распределение продаж по партиям (view-only)
- movements.py: Журнал складских операций (view-only)
"""
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .warehouse import WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView
from .incoming import IncomingListView, IncomingCreateView, IncomingUpdateView, IncomingDeleteView
from .batch import IncomingBatchListView, IncomingBatchDetailView, StockBatchListView, StockBatchDetailView
from .sale import SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView
from .inventory_ops import (
InventoryListView, InventoryCreateView, InventoryDetailView,
InventoryLineCreateBulkView
)
from .writeoff import WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView
from .transfer import TransferListView, TransferCreateView, TransferUpdateView, TransferDeleteView
from .reservation import ReservationListView, ReservationCreateView, ReservationUpdateView
from .stock import StockListView, StockDetailView
from .allocation import SaleBatchAllocationListView
from .movements import StockMovementListView
@login_required
def inventory_home(request):
"""
Главная страница Склада для управления инвентаризацией
"""
return render(request, 'inventory/home.html')
__all__ = [
# Home
'inventory_home',
# Warehouse
'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView',
# Incoming
'IncomingListView', 'IncomingCreateView', 'IncomingUpdateView', 'IncomingDeleteView',
# IncomingBatch
'IncomingBatchListView', 'IncomingBatchDetailView',
# Sale
'SaleListView', 'SaleCreateView', 'SaleUpdateView', 'SaleDeleteView', 'SaleDetailView',
# Inventory
'InventoryListView', 'InventoryCreateView', 'InventoryDetailView', 'InventoryLineCreateBulkView',
# WriteOff
'WriteOffListView', 'WriteOffCreateView', 'WriteOffUpdateView', 'WriteOffDeleteView',
# Transfer
'TransferListView', 'TransferCreateView', 'TransferUpdateView', 'TransferDeleteView',
# Reservation
'ReservationListView', 'ReservationCreateView', 'ReservationUpdateView',
# Stock
'StockListView', 'StockDetailView',
# StockBatch
'StockBatchListView', 'StockBatchDetailView',
# Allocation
'SaleBatchAllocationListView',
# Movement
'StockMovementListView',
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""
SaleBatchAllocation (Распределение продаж по партиям) views - READ ONLY
GROUP 3: LOW PRIORITY - Аудит и трассировка FIFO
"""
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import SaleBatchAllocation
class SaleBatchAllocationListView(LoginRequiredMixin, ListView):
"""
Полный список всех распределений продаж по партиям.
Используется для аудита и понимания как применялся FIFO.
"""
model = SaleBatchAllocation
template_name = 'inventory/allocation/allocation_list.html'
context_object_name = 'allocations'
paginate_by = 30
def get_queryset(self):
return SaleBatchAllocation.objects.select_related(
'sale', 'sale__product',
'batch', 'batch__product'
).order_by('-sale__date')

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""
Batch views - READ ONLY
- IncomingBatch (Партии поступлений)
- StockBatch (Партии товара на складе)
"""
from django.views.generic import ListView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import IncomingBatch, Incoming, StockBatch, SaleBatchAllocation, WriteOff
class IncomingBatchListView(LoginRequiredMixin, ListView):
"""Список всех партий поступлений товара"""
model = IncomingBatch
template_name = 'inventory/incoming_batch/batch_list.html'
context_object_name = 'batches'
paginate_by = 30
def get_queryset(self):
return IncomingBatch.objects.all().select_related('warehouse').order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Добавляем количество товаров в каждую партию
for batch in context['batches']:
batch.items_count = batch.items.count()
batch.total_quantity = sum(item.quantity for item in batch.items.all())
return context
class IncomingBatchDetailView(LoginRequiredMixin, DetailView):
"""Детальная информация по партии поступления"""
model = IncomingBatch
template_name = 'inventory/incoming_batch/batch_detail.html'
context_object_name = 'batch'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
batch = self.get_object()
# Товары в этой партии
context['items'] = batch.items.all().select_related('product', 'stock_batch')
return context
class StockBatchListView(LoginRequiredMixin, ListView):
"""Список всех партий товара на складах"""
model = StockBatch
template_name = 'inventory/batch/batch_list.html'
context_object_name = 'batches'
paginate_by = 30
def get_queryset(self):
return StockBatch.objects.filter(
is_active=True
).select_related('product', 'warehouse').order_by('-created_at')
class StockBatchDetailView(LoginRequiredMixin, DetailView):
"""
Детальная информация по партии товара.
Показывает историю операций с данной партией (продажи, списания, перемещения).
"""
model = StockBatch
template_name = 'inventory/batch/batch_detail.html'
context_object_name = 'batch'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
batch = self.get_object()
# История продаж из этой партии
context['sales'] = SaleBatchAllocation.objects.filter(
batch=batch
).select_related('sale', 'sale__product')
# История списаний из этой партии
context['writeoffs'] = WriteOff.objects.filter(
batch=batch
).order_by('-date')
return context

View File

@@ -0,0 +1,239 @@
# -*- 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
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
from ..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 = IncomingForm
template_name = 'inventory/incoming/incoming_form.html'
success_url = reverse_lazy('inventory:incoming-list')
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(is_active=True).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(is_active=True).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
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(is_active=True).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,
supplier_name=supplier_name,
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(is_active=True).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(is_active=True).order_by('name')
context = {
'form': form,
'products': products,
'products_json': request.POST.get('products_json', '[]'), # Сохраняем товары
}
return render(request, self.template_name, context)
def _parse_products_from_post(self, 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

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
from django.shortcuts import render, redirect
from django.views.generic import ListView, CreateView, DetailView, View, FormView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.http import HttpResponseRedirect
from ..models import Inventory, InventoryLine
from ..forms import InventoryForm, InventoryLineForm
class InventoryListView(LoginRequiredMixin, ListView):
"""
Список всех инвентаризаций по складам
"""
model = Inventory
template_name = 'inventory/inventory/inventory_list.html'
context_object_name = 'inventories'
paginate_by = 20
def get_queryset(self):
queryset = Inventory.objects.select_related('warehouse').order_by('-date')
# Фильтры (если переданы)
warehouse_id = self.request.GET.get('warehouse')
status = self.request.GET.get('status')
if warehouse_id:
queryset = queryset.filter(warehouse_id=warehouse_id)
if status:
queryset = queryset.filter(status=status)
return queryset
class InventoryCreateView(LoginRequiredMixin, CreateView):
"""
Начало новой инвентаризации по конкретному складу.
Переводит инвентаризацию в статус 'processing'.
"""
model = Inventory
form_class = InventoryForm
template_name = 'inventory/inventory/inventory_form.html'
success_url = reverse_lazy('inventory:inventory-list')
def form_valid(self, form):
form.instance.status = 'processing'
messages.success(
self.request,
f'Инвентаризация склада "{form.instance.warehouse.name}" начата.'
)
return super().form_valid(form)
class InventoryDetailView(LoginRequiredMixin, DetailView):
"""
Детальный просмотр инвентаризации с её строками.
Позволяет добавлять строки и заполнять фактические количества.
"""
model = Inventory
template_name = 'inventory/inventory/inventory_detail.html'
context_object_name = 'inventory'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Получаем все строки этой инвентаризации
context['lines'] = InventoryLine.objects.filter(
inventory=self.object
).select_related('product')
return context
class InventoryLineCreateBulkView(LoginRequiredMixin, View):
"""
Форма для массового внесения результатов инвентаризации.
Позволяет заполнить результаты пересчета для всех товаров на складе.
"""
template_name = 'inventory/inventory/inventory_line_bulk_form.html'
def get_context_data(self, inventory_id, **kwargs):
inventory = Inventory.objects.get(pk=inventory_id)
return {
'inventory': inventory,
'products': inventory.warehouse.stock_batches.values_list(
'product', flat=True
).distinct()
}
def get(self, request, pk):
inventory = Inventory.objects.get(pk=pk)
context = {
'inventory': inventory,
'lines': InventoryLine.objects.filter(inventory=inventory).select_related('product')
}
return render(request, self.template_name, context)
def post(self, request, pk):
inventory = Inventory.objects.get(pk=pk)
# Здесь будет логика обработки массового ввода данных
# TODO: Реализовать обработку формы с множественными строками
messages.success(request, 'Результаты инвентаризации добавлены.')
return redirect('inventory:inventory-detail', pk=pk)

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
StockMovement (Журнал всех складских операций) views - READ ONLY
GROUP 3: LOW PRIORITY - Аудит логирование
"""
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import StockMovement
class StockMovementListView(LoginRequiredMixin, ListView):
"""
Полный журнал всех складских операций (приход, продажа, списание, корректировка).
Используется для аудита и контроля.
"""
model = StockMovement
template_name = 'inventory/movements/movement_list.html'
context_object_name = 'movements'
paginate_by = 50
def get_queryset(self):
return StockMovement.objects.select_related(
'product', 'order'
).order_by('-created_at')

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""
Reservation (Резервирование товара) views
GROUP 2: MEDIUM PRIORITY
"""
from django.views.generic import ListView, CreateView, UpdateView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Reservation
from ..forms import ReservationForm
class ReservationListView(LoginRequiredMixin, ListView):
model = Reservation
template_name = 'inventory/reservation/reservation_list.html'
context_object_name = 'reservations'
paginate_by = 20
def get_queryset(self):
return Reservation.objects.filter(
status='reserved'
).select_related('product', 'warehouse', 'order_item').order_by('-reserved_at')
class ReservationCreateView(LoginRequiredMixin, CreateView):
model = Reservation
form_class = ReservationForm
template_name = 'inventory/reservation/reservation_form.html'
success_url = reverse_lazy('inventory:reservation-list')
def form_valid(self, form):
form.instance.status = 'reserved'
messages.success(self.request, f'Товар успешно зарезервирован.')
return super().form_valid(form)
class ReservationUpdateView(LoginRequiredMixin, UpdateView):
model = Reservation
fields = ['status']
template_name = 'inventory/reservation/reservation_update.html'
success_url = reverse_lazy('inventory:reservation-list')
def form_valid(self, form):
messages.success(self.request, f'Статус резервирования обновлен.')
return super().form_valid(form)

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Sale, SaleBatchAllocation
from ..forms import SaleForm
class SaleListView(LoginRequiredMixin, ListView):
"""
Список всех продаж товара (истории реализации)
"""
model = Sale
template_name = 'inventory/sale/sale_list.html'
context_object_name = 'sales'
paginate_by = 20
def get_queryset(self):
queryset = Sale.objects.select_related('product', 'warehouse', 'order').order_by('-date')
# Фильтры (если переданы)
product_id = self.request.GET.get('product')
warehouse_id = self.request.GET.get('warehouse')
processed = self.request.GET.get('processed')
if product_id:
queryset = queryset.filter(product_id=product_id)
if warehouse_id:
queryset = queryset.filter(warehouse_id=warehouse_id)
if processed:
queryset = queryset.filter(processed=processed == 'true')
return queryset
class SaleCreateView(LoginRequiredMixin, CreateView):
"""
Регистрация новой продажи товара.
После сохранения автоматически применяется FIFO (через сигнал).
"""
model = Sale
form_class = SaleForm
template_name = 'inventory/sale/sale_form.html'
success_url = reverse_lazy('inventory:sale-list')
def form_valid(self, form):
messages.success(
self.request,
f'Продажа товара "{form.instance.product.name}" ({form.instance.quantity} шт) успешно зарегистрирована.'
)
return super().form_valid(form)
class SaleDetailView(LoginRequiredMixin, DetailView):
"""
Просмотр деталей продажи с распределением по партиям.
Показывает SaleBatchAllocation для данной продажи.
"""
model = Sale
template_name = 'inventory/sale/sale_detail.html'
context_object_name = 'sale'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Получаем все распределения этой продажи по партиям
context['allocations'] = SaleBatchAllocation.objects.filter(
sale=self.object
).select_related('batch', 'batch__product')
return context
class SaleUpdateView(LoginRequiredMixin, UpdateView):
"""
Редактирование продажи (только если ещё не обработана).
Обработанные продажи редактировать нельзя.
"""
model = Sale
form_class = SaleForm
template_name = 'inventory/sale/sale_form.html'
success_url = reverse_lazy('inventory:sale-list')
def form_valid(self, form):
messages.success(self.request, f'Продажа товара обновлена.')
return super().form_valid(form)
class SaleDeleteView(LoginRequiredMixin, DeleteView):
"""
Отмена/удаление продажи товара.
"""
model = Sale
template_name = 'inventory/sale/sale_confirm_delete.html'
success_url = reverse_lazy('inventory:sale-list')
def form_valid(self, form):
sale = self.get_object()
messages.success(
self.request,
f'Продажа товара "{sale.product.name}" отменена.'
)
return super().form_valid(form)

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""
Stock (Остатки товаров) views - READ ONLY
GROUP 3: LOW PRIORITY - Справочник состояния
"""
from django.views.generic import ListView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import Stock
class StockListView(LoginRequiredMixin, ListView):
"""Список всех остатков товаров на всех складах"""
model = Stock
template_name = 'inventory/stock/stock_list.html'
context_object_name = 'stocks'
paginate_by = 30
def get_queryset(self):
# Показываем все остатки, включая нулевые (для полной видимости)
return Stock.objects.select_related('product', 'warehouse').order_by('warehouse', 'product')
class StockDetailView(LoginRequiredMixin, DetailView):
"""Детальная информация по остаткам конкретного товара"""
model = Stock
template_name = 'inventory/stock/stock_detail.html'
context_object_name = 'stock'

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""
Transfer (Перемещение товара между складами) views
GROUP 2: MEDIUM PRIORITY
"""
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Transfer
from ..forms import TransferForm
class TransferListView(LoginRequiredMixin, ListView):
model = Transfer
template_name = 'inventory/transfer/transfer_list.html'
context_object_name = 'transfers'
paginate_by = 20
def get_queryset(self):
return Transfer.objects.select_related(
'batch', 'batch__product',
'from_warehouse', 'to_warehouse'
).order_by('-date')
class TransferCreateView(LoginRequiredMixin, CreateView):
model = Transfer
form_class = TransferForm
template_name = 'inventory/transfer/transfer_form.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
messages.success(
self.request,
f'Перемещение товара "{form.instance.batch.product.name}" успешно зарегистрировано.'
)
return super().form_valid(form)
class TransferUpdateView(LoginRequiredMixin, UpdateView):
model = Transfer
form_class = TransferForm
template_name = 'inventory/transfer/transfer_form.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
messages.success(self.request, f'Перемещение товара обновлено.')
return super().form_valid(form)
class TransferDeleteView(LoginRequiredMixin, DeleteView):
model = Transfer
template_name = 'inventory/transfer/transfer_confirm_delete.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
transfer = self.get_object()
messages.success(self.request, f'Перемещение товара отменено.')
return super().form_valid(form)

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Warehouse
from ..forms import WarehouseForm
class WarehouseListView(LoginRequiredMixin, ListView):
"""
Список всех складов тенанта
"""
model = Warehouse
template_name = 'inventory/warehouse/warehouse_list.html'
context_object_name = 'warehouses'
paginate_by = 20
def get_queryset(self):
return Warehouse.objects.filter(is_active=True).order_by('name')
class WarehouseCreateView(LoginRequiredMixin, CreateView):
"""
Создание нового склада
"""
model = Warehouse
form_class = WarehouseForm
template_name = 'inventory/warehouse/warehouse_form.html'
success_url = reverse_lazy('inventory:warehouse-list')
def form_valid(self, form):
messages.success(self.request, f'Склад "{form.instance.name}" успешно создан.')
return super().form_valid(form)
class WarehouseUpdateView(LoginRequiredMixin, UpdateView):
"""
Редактирование склада
"""
model = Warehouse
form_class = WarehouseForm
template_name = 'inventory/warehouse/warehouse_form.html'
success_url = reverse_lazy('inventory:warehouse-list')
def form_valid(self, form):
messages.success(self.request, f'Склад "{form.instance.name}" успешно обновлён.')
return super().form_valid(form)
class WarehouseDeleteView(LoginRequiredMixin, DeleteView):
"""
Удаление склада (мягкое удаление - деактивация)
"""
model = Warehouse
template_name = 'inventory/warehouse/warehouse_confirm_delete.html'
success_url = reverse_lazy('inventory:warehouse-list')
def form_valid(self, form):
# Мягкое удаление - просто деактивируем
warehouse = self.get_object()
warehouse.is_active = False
warehouse.save()
messages.success(self.request, f'Склад "{warehouse.name}" деактивирован.')
return super().form_valid(form)

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""
WriteOff (Списание товара) views
GROUP 2: MEDIUM PRIORITY
"""
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import WriteOff
from ..forms import WriteOffForm
class WriteOffListView(LoginRequiredMixin, ListView):
model = WriteOff
template_name = 'inventory/writeoff/writeoff_list.html'
context_object_name = 'writeoffs'
paginate_by = 20
def get_queryset(self):
return WriteOff.objects.select_related('batch', 'batch__product').order_by('-date')
class WriteOffCreateView(LoginRequiredMixin, CreateView):
model = WriteOff
form_class = WriteOffForm
template_name = 'inventory/writeoff/writeoff_form.html'
success_url = reverse_lazy('inventory:writeoff-list')
def form_valid(self, form):
messages.success(self.request, f'Списание товара успешно создано.')
return super().form_valid(form)
class WriteOffUpdateView(LoginRequiredMixin, UpdateView):
model = WriteOff
form_class = WriteOffForm
template_name = 'inventory/writeoff/writeoff_form.html'
success_url = reverse_lazy('inventory:writeoff-list')
def form_valid(self, form):
messages.success(self.request, f'Списание товара обновлено.')
return super().form_valid(form)
class WriteOffDeleteView(LoginRequiredMixin, DeleteView):
model = WriteOff
template_name = 'inventory/writeoff/writeoff_confirm_delete.html'
success_url = reverse_lazy('inventory:writeoff-list')
def form_valid(self, form):
writeoff = self.get_object()
messages.success(self.request, f'Списание товара отменено.')
return super().form_valid(form)