refactor: стандартизация моделей документов перемещения

Приведение к единому паттерну именования документов:
- TransferBatch → TransferDocument
- TransferItem → TransferDocumentItem
- Удалена устаревшая модель Transfer (одиночные перемещения)
- Удалена неиспользуемая модель StockMovement

Изменения:
- models.py: переименование классов, обновление related_names
- admin.py: удаление регистраций Transfer/StockMovement
- forms.py: обновление TransferHeaderForm
- views/transfer.py: обновление всех view классов
- templates: замена transfer_batch → transfer_document
- urls.py: удаление путей для movements
- views/__init__.py: удаление импорта StockMovementListView
- views/movements.py: удален файл

Миграция: 0005_refactor_transfer_models
- RenameModel операции для сохранения данных
- DeleteModel для Transfer и StockMovement

Единый паттерн: *Document + *DocumentItem
(WriteOffDocument, IncomingDocument, TransferDocument)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 20:29:11 +03:00
parent c534e27c41
commit 08bae834c8
9 changed files with 84 additions and 170 deletions

View File

@@ -5,11 +5,11 @@ from django.db.models import Sum
from decimal import Decimal
from inventory.models import (
Warehouse, StockBatch, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement,
Warehouse, StockBatch, Sale, WriteOff,
Inventory, InventoryLine, Reservation, Stock,
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
TransformationOutput
TransformationOutput, TransferDocument, TransferDocumentItem
)
@@ -166,28 +166,6 @@ class WriteOffAdmin(admin.ModelAdmin):
reason_display.short_description = 'Причина'
# ===== TRANSFER =====
@admin.register(Transfer)
class TransferAdmin(admin.ModelAdmin):
list_display = ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'date')
list_filter = ('date', 'from_warehouse', 'to_warehouse')
search_fields = ('batch__product__name', 'document_number')
date_hierarchy = 'date'
fieldsets = (
('Перемещение', {
'fields': ('batch', 'from_warehouse', 'to_warehouse', 'quantity', 'new_batch')
}),
('Документ', {
'fields': ('document_number',)
}),
('Дата', {
'fields': ('date',),
'classes': ('collapse',)
}),
)
readonly_fields = ('date', 'new_batch')
# ===== INVENTORY LINE (INLINE) =====
class InventoryLineInline(admin.TabularInline):
model = InventoryLine
@@ -317,16 +295,6 @@ class StockAdmin(admin.ModelAdmin):
readonly_fields = ('quantity_available', 'quantity_reserved', 'updated_at')
# ===== STOCK MOVEMENT (для аудита) =====
@admin.register(StockMovement)
class StockMovementAdmin(admin.ModelAdmin):
list_display = ('product', 'change', 'reason', 'order', 'created_at')
list_filter = ('reason', 'created_at')
search_fields = ('product__name', 'order__order_number')
date_hierarchy = 'created_at'
readonly_fields = ('created_at',)
# ===== WRITEOFF DOCUMENT (документы списания) =====
class WriteOffDocumentItemInline(admin.TabularInline):
model = WriteOffDocumentItem

View File

@@ -4,8 +4,8 @@ from django.core.exceptions import ValidationError
from decimal import Decimal
from .models import (
Warehouse, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
Warehouse, Sale, WriteOff, Reservation, Inventory, InventoryLine, StockBatch,
TransferDocument, TransferDocumentItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput
)
from products.models import Product
@@ -157,7 +157,7 @@ class TransferHeaderForm(forms.ModelForm):
Содержит информацию о складах-источнике и складе-назначении, примечания.
"""
class Meta:
model = TransferBatch
model = TransferDocument
fields = ['from_warehouse', 'to_warehouse', 'notes']
widgets = {
'from_warehouse': forms.Select(attrs={'class': 'form-control'}),

View File

@@ -0,0 +1,38 @@
# Generated migration for Transfer models refactoring
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_remove_incoming_batch_and_incoming'),
('products', '0001_initial'),
]
operations = [
# 1. Удаление устаревших моделей ПЕРЕД переименованием
migrations.DeleteModel(
name='Transfer',
),
migrations.DeleteModel(
name='StockMovement',
),
# 2. Переименование моделей
migrations.RenameModel(
old_name='TransferBatch',
new_name='TransferDocument',
),
migrations.RenameModel(
old_name='TransferItem',
new_name='TransferDocumentItem',
),
# 3. Переименование поля transfer_batch → transfer_document в TransferDocumentItem
migrations.RenameField(
model_name='transferdocumentitem',
old_name='transfer_batch',
new_name='transfer_document',
),
]

View File

@@ -215,35 +215,6 @@ class WriteOff(models.Model):
super().save(*args, **kwargs)
class Transfer(models.Model):
"""
Перемещение товара между складами. Сохраняет партийность.
"""
batch = models.ForeignKey(StockBatch, on_delete=models.CASCADE,
related_name='transfers', verbose_name="Партия")
from_warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
related_name='transfers_from', verbose_name="Из склада")
to_warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
related_name='transfers_to', verbose_name="На склад")
quantity = models.DecimalField(max_digits=10, decimal_places=3, verbose_name="Количество")
document_number = models.CharField(max_length=100, blank=True, null=True,
verbose_name="Номер документа")
date = models.DateTimeField(auto_now_add=True, verbose_name="Дата операции")
new_batch = models.ForeignKey(StockBatch, on_delete=models.SET_NULL, null=True, blank=True,
related_name='transfer_sources', verbose_name="Новая партия")
class Meta:
verbose_name = "Перемещение"
verbose_name_plural = "Перемещения"
ordering = ['-date']
indexes = [
models.Index(fields=['from_warehouse', 'to_warehouse']),
models.Index(fields=['date']),
]
def __str__(self):
return f"Перемещение {self.batch.product.name} ({self.quantity} шт): {self.from_warehouse}{self.to_warehouse}"
class Inventory(models.Model):
"""
@@ -731,38 +702,6 @@ class Stock(models.Model):
self.save()
class StockMovement(models.Model):
"""
Журнал всех складских операций (приход, списание, коррекция).
Используется для аудита.
"""
REASON_CHOICES = [
('purchase', 'Закупка'),
('sale', 'Продажа'),
('write_off', 'Списание'),
('adjustment', 'Корректировка'),
]
product = models.ForeignKey(Product, on_delete=models.CASCADE,
related_name='movements', verbose_name="Товар")
change = models.DecimalField(max_digits=10, decimal_places=3, verbose_name="Изменение")
reason = models.CharField(max_length=20, choices=REASON_CHOICES, verbose_name="Причина")
order = models.ForeignKey('orders.Order', on_delete=models.SET_NULL, null=True, blank=True,
related_name='stock_movements', verbose_name="Заказ")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
class Meta:
verbose_name = "Движение товара"
verbose_name_plural = "Движения товаров"
indexes = [
models.Index(fields=['product']),
models.Index(fields=['created_at']),
]
def __str__(self):
return f"{self.product.name}: {self.change} ({self.reason})"
class DocumentCounter(models.Model):
"""
Счетчик номеров документов для различных операций.
@@ -811,7 +750,7 @@ class DocumentCounter(models.Model):
return obj.current_value
class TransferBatch(models.Model):
class TransferDocument(models.Model):
"""
Документ перемещения товара между складами.
Один номер документа = одна операция перемещения множественных товаров.
@@ -819,13 +758,13 @@ class TransferBatch(models.Model):
from_warehouse = models.ForeignKey(
Warehouse,
on_delete=models.CASCADE,
related_name='transfer_batches_from',
related_name='transfer_documents_from',
verbose_name="Склад-отгрузки"
)
to_warehouse = models.ForeignKey(
Warehouse,
on_delete=models.CASCADE,
related_name='transfer_batches_to',
related_name='transfer_documents_to',
verbose_name="Склад-приемки"
)
document_number = models.CharField(
@@ -866,13 +805,13 @@ class TransferBatch(models.Model):
return f"Перемещение {self.document_number}: {total_items} товаров, {total_qty} шт ({self.from_warehouse}{self.to_warehouse})"
class TransferItem(models.Model):
class TransferDocumentItem(models.Model):
"""
Строка документа перемещения (товар в перемещении).
Связь между документом и товарами.
"""
transfer_batch = models.ForeignKey(
TransferBatch,
transfer_document = models.ForeignKey(
TransferDocument,
on_delete=models.CASCADE,
related_name='items',
verbose_name="Документ перемещения"
@@ -880,13 +819,13 @@ class TransferItem(models.Model):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='transfer_items',
related_name='transfer_document_items',
verbose_name="Товар"
)
batch = models.ForeignKey(
StockBatch,
on_delete=models.CASCADE,
related_name='transfer_items',
related_name='transfer_document_items',
verbose_name="Исходная партия (FIFO)"
)
quantity = models.DecimalField(
@@ -899,17 +838,17 @@ class TransferItem(models.Model):
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='transfer_items_created',
related_name='transfer_document_items_created',
verbose_name="Созданная партия на целевом складе"
)
class Meta:
verbose_name = "Строка перемещения"
verbose_name_plural = "Строки перемещения"
unique_together = [['transfer_batch', 'batch']]
unique_together = [['transfer_document', 'batch']]
ordering = ['id']
indexes = [
models.Index(fields=['transfer_batch']),
models.Index(fields=['transfer_document']),
models.Index(fields=['product']),
]

View File

@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block title %}Документ перемещения {{ transfer_batch.document_number }}{% endblock %}
{% block title %}Документ перемещения {{ transfer_document.document_number }}{% endblock %}
{% block content %}
<div class="container-fluid px-4 py-3">
@@ -8,7 +8,7 @@
<nav aria-label="breadcrumb" class="mb-2">
<ol class="breadcrumb breadcrumb-sm mb-0">
<li class="breadcrumb-item"><a href="{% url 'inventory:transfer-list' %}">Перемещения</a></li>
<li class="breadcrumb-item active">{{ transfer_batch.document_number }}</li>
<li class="breadcrumb-item active">{{ transfer_document.document_number }}</li>
</ol>
</nav>
@@ -18,36 +18,36 @@
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-light py-3">
<h5 class="mb-0">
<i class="bi bi-arrow-left-right me-2"></i>{{ transfer_batch.document_number }}
<i class="bi bi-arrow-left-right me-2"></i>{{ transfer_document.document_number }}
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<p class="text-muted small mb-1">Склад-отгрузки</p>
<p class="fw-semibold">{{ transfer_batch.from_warehouse.name }}</p>
<p class="fw-semibold">{{ transfer_document.from_warehouse.name }}</p>
</div>
<div class="col-md-6">
<p class="text-muted small mb-1">Склад-приемки</p>
<p class="fw-semibold">{{ transfer_batch.to_warehouse.name }}</p>
<p class="fw-semibold">{{ transfer_document.to_warehouse.name }}</p>
</div>
</div>
{% if transfer_batch.notes %}
{% if transfer_document.notes %}
<div class="mb-3">
<p class="text-muted small mb-1">Примечания</p>
<p>{{ transfer_batch.notes }}</p>
<p>{{ transfer_document.notes }}</p>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<p class="text-muted small mb-1">Дата создания</p>
<p class="fw-semibold">{{ transfer_batch.created_at|date:"d.m.Y H:i" }}</p>
<p class="fw-semibold">{{ transfer_document.created_at|date:"d.m.Y H:i" }}</p>
</div>
<div class="col-md-6">
<p class="text-muted small mb-1">Последнее обновление</p>
<p class="fw-semibold">{{ transfer_batch.updated_at|date:"d.m.Y H:i" }}</p>
<p class="fw-semibold">{{ transfer_document.updated_at|date:"d.m.Y H:i" }}</p>
</div>
</div>
</div>
@@ -132,7 +132,7 @@
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Вернуться к списку
</a>
<a href="{% url 'inventory:transfer-delete' transfer_batch.id %}" class="btn btn-outline-danger btn-sm">
<a href="{% url 'inventory:transfer-delete' transfer_document.id %}" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash me-1"></i>Удалить
</a>
</div>

View File

@@ -21,8 +21,6 @@ from .views import (
StockBatchListView, StockBatchDetailView,
# SaleBatchAllocation
SaleBatchAllocationListView,
# StockMovement
StockMovementListView,
)
# Showcase views
from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView
@@ -131,9 +129,6 @@ urlpatterns = [
# ==================== ALLOCATION (READ ONLY) ====================
path('allocations/', SaleBatchAllocationListView.as_view(), name='allocation-list'),
# ==================== MOVEMENT (READ ONLY) ====================
path('movements/', StockMovementListView.as_view(), name='movement-list'),
# ==================== SHOWCASE ====================
path('showcases/', ShowcaseListView.as_view(), name='showcase-list'),
path('showcases/create/', ShowcaseCreateView.as_view(), name='showcase-create'),

View File

@@ -13,7 +13,6 @@ Inventory Views Package
- 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
@@ -41,7 +40,6 @@ from .transfer import TransferListView, TransferBulkCreateView, TransferDetailVi
from .reservation import ReservationListView
from .stock import StockListView, StockDetailView
from .allocation import SaleBatchAllocationListView
from .movements import StockMovementListView
@login_required

View File

@@ -1,24 +0,0 @@
# -*- 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

@@ -13,7 +13,7 @@ from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction
from django.shortcuts import redirect, render
from ..models import TransferBatch, TransferItem, Stock
from ..models import TransferDocument, TransferDocumentItem, Stock
from ..forms import TransferBulkForm
from inventory.utils.document_generator import generate_transfer_document_number
from inventory.services.batch_manager import StockBatchManager
@@ -24,19 +24,19 @@ class TransferListView(LoginRequiredMixin, ListView):
"""
View для просмотра списка документов перемещений товаров.
"""
model = TransferBatch
model = TransferDocument
template_name = 'inventory/transfer/transfer_list.html'
context_object_name = 'transfers'
paginate_by = 20
def get_queryset(self):
return TransferBatch.objects.select_related(
return TransferDocument.objects.select_related(
'from_warehouse', 'to_warehouse'
).order_by('-created_at')
# ============================================================================
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferBatch + TransferItem)
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferDocument + TransferDocumentItem)
# ============================================================================
class TransferBulkCreateView(LoginRequiredMixin, View):
@@ -100,8 +100,8 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
# Начинаем транзакцию
try:
with transaction.atomic():
# 1. Создаем документ TransferBatch
transfer_batch = TransferBatch.objects.create(
# 1. Создаем документ TransferDocument
transfer_document = TransferDocument.objects.create(
from_warehouse=from_warehouse,
to_warehouse=to_warehouse,
document_number=generate_transfer_document_number(),
@@ -119,10 +119,10 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
quantity=quantity
)
# Создаем TransferItem для каждого использованного batch
# Создаем TransferDocumentItem для каждого использованного batch
for source_batch, qty_transferred, new_batch in transfers:
TransferItem.objects.create(
transfer_batch=transfer_batch,
TransferDocumentItem.objects.create(
transfer_document=transfer_document,
product=product,
batch=source_batch,
quantity=qty_transferred,
@@ -136,11 +136,11 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
# 3. Успешно создали документ
messages.success(
request,
f'Документ перемещения {transfer_batch.document_number} успешно создан. '
f'Документ перемещения {transfer_document.document_number} успешно создан. '
f'Перемещено {len(products)} видов товаров.'
)
return redirect('inventory:transfer-detail', pk=transfer_batch.id)
return redirect('inventory:transfer-detail', pk=transfer_document.id)
except Exception as e:
messages.error(request, f'Ошибка при создании документа перемещения: {str(e)}')
@@ -151,12 +151,12 @@ class TransferDetailView(LoginRequiredMixin, DetailView):
"""
View для просмотра деталей документа перемещения.
"""
model = TransferBatch
model = TransferDocument
template_name = 'inventory/transfer/transfer_detail.html'
context_object_name = 'transfer_batch'
context_object_name = 'transfer_document'
def get_queryset(self):
return TransferBatch.objects.select_related(
return TransferDocument.objects.select_related(
'from_warehouse', 'to_warehouse'
).prefetch_related(
'items__product', 'items__batch', 'items__new_batch'
@@ -164,10 +164,10 @@ class TransferDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
transfer_batch = self.object
transfer_document = self.object
# Собираем статистику по документу
items = transfer_batch.items.all()
items = transfer_document.items.all()
total_items = items.count()
total_qty = sum(Decimal(str(item.quantity)) for item in items)
@@ -182,13 +182,13 @@ class TransferDeleteView(LoginRequiredMixin, DeleteView):
"""
View для удаления документа перемещения.
"""
model = TransferBatch
model = TransferDocument
template_name = 'inventory/transfer/transfer_confirm_delete.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
transfer_batch = self.get_object()
messages.success(self.request, f'Документ перемещения {transfer_batch.document_number} удалён.')
transfer_document = self.get_object()
messages.success(self.request, f'Документ перемещения {transfer_document.document_number} удалён.')
return super().form_valid(form)