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:
@@ -5,11 +5,11 @@ from django.db.models import Sum
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from inventory.models import (
|
from inventory.models import (
|
||||||
Warehouse, StockBatch, Sale, WriteOff, Transfer,
|
Warehouse, StockBatch, Sale, WriteOff,
|
||||||
Inventory, InventoryLine, Reservation, Stock, StockMovement,
|
Inventory, InventoryLine, Reservation, Stock,
|
||||||
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
|
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
|
||||||
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
|
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,
|
||||||
TransformationOutput
|
TransformationOutput, TransferDocument, TransferDocumentItem
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -166,28 +166,6 @@ class WriteOffAdmin(admin.ModelAdmin):
|
|||||||
reason_display.short_description = 'Причина'
|
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) =====
|
# ===== INVENTORY LINE (INLINE) =====
|
||||||
class InventoryLineInline(admin.TabularInline):
|
class InventoryLineInline(admin.TabularInline):
|
||||||
model = InventoryLine
|
model = InventoryLine
|
||||||
@@ -317,16 +295,6 @@ class StockAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ('quantity_available', 'quantity_reserved', 'updated_at')
|
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 (документы списания) =====
|
# ===== WRITEOFF DOCUMENT (документы списания) =====
|
||||||
class WriteOffDocumentItemInline(admin.TabularInline):
|
class WriteOffDocumentItemInline(admin.TabularInline):
|
||||||
model = WriteOffDocumentItem
|
model = WriteOffDocumentItem
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from django.core.exceptions import ValidationError
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Warehouse, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
|
Warehouse, Sale, WriteOff, Reservation, Inventory, InventoryLine, StockBatch,
|
||||||
TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
|
TransferDocument, TransferDocumentItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
|
||||||
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput
|
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput
|
||||||
)
|
)
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
@@ -157,7 +157,7 @@ class TransferHeaderForm(forms.ModelForm):
|
|||||||
Содержит информацию о складах-источнике и складе-назначении, примечания.
|
Содержит информацию о складах-источнике и складе-назначении, примечания.
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TransferBatch
|
model = TransferDocument
|
||||||
fields = ['from_warehouse', 'to_warehouse', 'notes']
|
fields = ['from_warehouse', 'to_warehouse', 'notes']
|
||||||
widgets = {
|
widgets = {
|
||||||
'from_warehouse': forms.Select(attrs={'class': 'form-control'}),
|
'from_warehouse': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -215,35 +215,6 @@ class WriteOff(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
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):
|
class Inventory(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -731,38 +702,6 @@ class Stock(models.Model):
|
|||||||
self.save()
|
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):
|
class DocumentCounter(models.Model):
|
||||||
"""
|
"""
|
||||||
Счетчик номеров документов для различных операций.
|
Счетчик номеров документов для различных операций.
|
||||||
@@ -811,7 +750,7 @@ class DocumentCounter(models.Model):
|
|||||||
return obj.current_value
|
return obj.current_value
|
||||||
|
|
||||||
|
|
||||||
class TransferBatch(models.Model):
|
class TransferDocument(models.Model):
|
||||||
"""
|
"""
|
||||||
Документ перемещения товара между складами.
|
Документ перемещения товара между складами.
|
||||||
Один номер документа = одна операция перемещения множественных товаров.
|
Один номер документа = одна операция перемещения множественных товаров.
|
||||||
@@ -819,13 +758,13 @@ class TransferBatch(models.Model):
|
|||||||
from_warehouse = models.ForeignKey(
|
from_warehouse = models.ForeignKey(
|
||||||
Warehouse,
|
Warehouse,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='transfer_batches_from',
|
related_name='transfer_documents_from',
|
||||||
verbose_name="Склад-отгрузки"
|
verbose_name="Склад-отгрузки"
|
||||||
)
|
)
|
||||||
to_warehouse = models.ForeignKey(
|
to_warehouse = models.ForeignKey(
|
||||||
Warehouse,
|
Warehouse,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='transfer_batches_to',
|
related_name='transfer_documents_to',
|
||||||
verbose_name="Склад-приемки"
|
verbose_name="Склад-приемки"
|
||||||
)
|
)
|
||||||
document_number = models.CharField(
|
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})"
|
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(
|
transfer_document = models.ForeignKey(
|
||||||
TransferBatch,
|
TransferDocument,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='items',
|
related_name='items',
|
||||||
verbose_name="Документ перемещения"
|
verbose_name="Документ перемещения"
|
||||||
@@ -880,13 +819,13 @@ class TransferItem(models.Model):
|
|||||||
product = models.ForeignKey(
|
product = models.ForeignKey(
|
||||||
Product,
|
Product,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='transfer_items',
|
related_name='transfer_document_items',
|
||||||
verbose_name="Товар"
|
verbose_name="Товар"
|
||||||
)
|
)
|
||||||
batch = models.ForeignKey(
|
batch = models.ForeignKey(
|
||||||
StockBatch,
|
StockBatch,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='transfer_items',
|
related_name='transfer_document_items',
|
||||||
verbose_name="Исходная партия (FIFO)"
|
verbose_name="Исходная партия (FIFO)"
|
||||||
)
|
)
|
||||||
quantity = models.DecimalField(
|
quantity = models.DecimalField(
|
||||||
@@ -899,17 +838,17 @@ class TransferItem(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='transfer_items_created',
|
related_name='transfer_document_items_created',
|
||||||
verbose_name="Созданная партия на целевом складе"
|
verbose_name="Созданная партия на целевом складе"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Строка перемещения"
|
verbose_name = "Строка перемещения"
|
||||||
verbose_name_plural = "Строки перемещения"
|
verbose_name_plural = "Строки перемещения"
|
||||||
unique_together = [['transfer_batch', 'batch']]
|
unique_together = [['transfer_document', 'batch']]
|
||||||
ordering = ['id']
|
ordering = ['id']
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['transfer_batch']),
|
models.Index(fields=['transfer_document']),
|
||||||
models.Index(fields=['product']),
|
models.Index(fields=['product']),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Документ перемещения {{ transfer_batch.document_number }}{% endblock %}
|
{% block title %}Документ перемещения {{ transfer_document.document_number }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid px-4 py-3">
|
<div class="container-fluid px-4 py-3">
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<nav aria-label="breadcrumb" class="mb-2">
|
<nav aria-label="breadcrumb" class="mb-2">
|
||||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inventory:transfer-list' %}">Перемещения</a></li>
|
<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>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -18,36 +18,36 @@
|
|||||||
<div class="card border-0 shadow-sm mb-3">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-header bg-light py-3">
|
<div class="card-header bg-light py-3">
|
||||||
<h5 class="mb-0">
|
<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>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p class="text-muted small mb-1">Склад-отгрузки</p>
|
<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>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p class="text-muted small mb-1">Склад-приемки</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if transfer_batch.notes %}
|
{% if transfer_document.notes %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="text-muted small mb-1">Примечания</p>
|
<p class="text-muted small mb-1">Примечания</p>
|
||||||
<p>{{ transfer_batch.notes }}</p>
|
<p>{{ transfer_document.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p class="text-muted small mb-1">Дата создания</p>
|
<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>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p class="text-muted small mb-1">Последнее обновление</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary btn-sm">
|
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left me-1"></i>Вернуться к списку
|
<i class="bi bi-arrow-left me-1"></i>Вернуться к списку
|
||||||
</a>
|
</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>Удалить
|
<i class="bi bi-trash me-1"></i>Удалить
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ from .views import (
|
|||||||
StockBatchListView, StockBatchDetailView,
|
StockBatchListView, StockBatchDetailView,
|
||||||
# SaleBatchAllocation
|
# SaleBatchAllocation
|
||||||
SaleBatchAllocationListView,
|
SaleBatchAllocationListView,
|
||||||
# StockMovement
|
|
||||||
StockMovementListView,
|
|
||||||
)
|
)
|
||||||
# Showcase views
|
# Showcase views
|
||||||
from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView
|
from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView
|
||||||
@@ -131,9 +129,6 @@ urlpatterns = [
|
|||||||
# ==================== ALLOCATION (READ ONLY) ====================
|
# ==================== ALLOCATION (READ ONLY) ====================
|
||||||
path('allocations/', SaleBatchAllocationListView.as_view(), name='allocation-list'),
|
path('allocations/', SaleBatchAllocationListView.as_view(), name='allocation-list'),
|
||||||
|
|
||||||
# ==================== MOVEMENT (READ ONLY) ====================
|
|
||||||
path('movements/', StockMovementListView.as_view(), name='movement-list'),
|
|
||||||
|
|
||||||
# ==================== SHOWCASE ====================
|
# ==================== SHOWCASE ====================
|
||||||
path('showcases/', ShowcaseListView.as_view(), name='showcase-list'),
|
path('showcases/', ShowcaseListView.as_view(), name='showcase-list'),
|
||||||
path('showcases/create/', ShowcaseCreateView.as_view(), name='showcase-create'),
|
path('showcases/create/', ShowcaseCreateView.as_view(), name='showcase-create'),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Inventory Views Package
|
|||||||
- stock.py: Справочник остатков (view-only)
|
- stock.py: Справочник остатков (view-only)
|
||||||
- batch.py: Справочник партий товара (view-only)
|
- batch.py: Справочник партий товара (view-only)
|
||||||
- allocation.py: Распределение продаж по партиям (view-only)
|
- allocation.py: Распределение продаж по партиям (view-only)
|
||||||
- movements.py: Журнал складских операций (view-only)
|
|
||||||
"""
|
"""
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
@@ -41,7 +40,6 @@ from .transfer import TransferListView, TransferBulkCreateView, TransferDetailVi
|
|||||||
from .reservation import ReservationListView
|
from .reservation import ReservationListView
|
||||||
from .stock import StockListView, StockDetailView
|
from .stock import StockListView, StockDetailView
|
||||||
from .allocation import SaleBatchAllocationListView
|
from .allocation import SaleBatchAllocationListView
|
||||||
from .movements import StockMovementListView
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -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')
|
|
||||||
@@ -13,7 +13,7 @@ from django.contrib import messages
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from ..models import TransferBatch, TransferItem, Stock
|
from ..models import TransferDocument, TransferDocumentItem, Stock
|
||||||
from ..forms import TransferBulkForm
|
from ..forms import TransferBulkForm
|
||||||
from inventory.utils.document_generator import generate_transfer_document_number
|
from inventory.utils.document_generator import generate_transfer_document_number
|
||||||
from inventory.services.batch_manager import StockBatchManager
|
from inventory.services.batch_manager import StockBatchManager
|
||||||
@@ -24,19 +24,19 @@ class TransferListView(LoginRequiredMixin, ListView):
|
|||||||
"""
|
"""
|
||||||
View для просмотра списка документов перемещений товаров.
|
View для просмотра списка документов перемещений товаров.
|
||||||
"""
|
"""
|
||||||
model = TransferBatch
|
model = TransferDocument
|
||||||
template_name = 'inventory/transfer/transfer_list.html'
|
template_name = 'inventory/transfer/transfer_list.html'
|
||||||
context_object_name = 'transfers'
|
context_object_name = 'transfers'
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return TransferBatch.objects.select_related(
|
return TransferDocument.objects.select_related(
|
||||||
'from_warehouse', 'to_warehouse'
|
'from_warehouse', 'to_warehouse'
|
||||||
).order_by('-created_at')
|
).order_by('-created_at')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferBatch + TransferItem)
|
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferDocument + TransferDocumentItem)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
class TransferBulkCreateView(LoginRequiredMixin, View):
|
class TransferBulkCreateView(LoginRequiredMixin, View):
|
||||||
@@ -100,8 +100,8 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
|
|||||||
# Начинаем транзакцию
|
# Начинаем транзакцию
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 1. Создаем документ TransferBatch
|
# 1. Создаем документ TransferDocument
|
||||||
transfer_batch = TransferBatch.objects.create(
|
transfer_document = TransferDocument.objects.create(
|
||||||
from_warehouse=from_warehouse,
|
from_warehouse=from_warehouse,
|
||||||
to_warehouse=to_warehouse,
|
to_warehouse=to_warehouse,
|
||||||
document_number=generate_transfer_document_number(),
|
document_number=generate_transfer_document_number(),
|
||||||
@@ -119,10 +119,10 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
|
|||||||
quantity=quantity
|
quantity=quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
# Создаем TransferItem для каждого использованного batch
|
# Создаем TransferDocumentItem для каждого использованного batch
|
||||||
for source_batch, qty_transferred, new_batch in transfers:
|
for source_batch, qty_transferred, new_batch in transfers:
|
||||||
TransferItem.objects.create(
|
TransferDocumentItem.objects.create(
|
||||||
transfer_batch=transfer_batch,
|
transfer_document=transfer_document,
|
||||||
product=product,
|
product=product,
|
||||||
batch=source_batch,
|
batch=source_batch,
|
||||||
quantity=qty_transferred,
|
quantity=qty_transferred,
|
||||||
@@ -136,11 +136,11 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
|
|||||||
# 3. Успешно создали документ
|
# 3. Успешно создали документ
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
f'Документ перемещения {transfer_batch.document_number} успешно создан. '
|
f'Документ перемещения {transfer_document.document_number} успешно создан. '
|
||||||
f'Перемещено {len(products)} видов товаров.'
|
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:
|
except Exception as e:
|
||||||
messages.error(request, f'Ошибка при создании документа перемещения: {str(e)}')
|
messages.error(request, f'Ошибка при создании документа перемещения: {str(e)}')
|
||||||
@@ -151,12 +151,12 @@ class TransferDetailView(LoginRequiredMixin, DetailView):
|
|||||||
"""
|
"""
|
||||||
View для просмотра деталей документа перемещения.
|
View для просмотра деталей документа перемещения.
|
||||||
"""
|
"""
|
||||||
model = TransferBatch
|
model = TransferDocument
|
||||||
template_name = 'inventory/transfer/transfer_detail.html'
|
template_name = 'inventory/transfer/transfer_detail.html'
|
||||||
context_object_name = 'transfer_batch'
|
context_object_name = 'transfer_document'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return TransferBatch.objects.select_related(
|
return TransferDocument.objects.select_related(
|
||||||
'from_warehouse', 'to_warehouse'
|
'from_warehouse', 'to_warehouse'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'items__product', 'items__batch', 'items__new_batch'
|
'items__product', 'items__batch', 'items__new_batch'
|
||||||
@@ -164,10 +164,10 @@ class TransferDetailView(LoginRequiredMixin, DetailView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**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_items = items.count()
|
||||||
total_qty = sum(Decimal(str(item.quantity)) for item in items)
|
total_qty = sum(Decimal(str(item.quantity)) for item in items)
|
||||||
|
|
||||||
@@ -182,13 +182,13 @@ class TransferDeleteView(LoginRequiredMixin, DeleteView):
|
|||||||
"""
|
"""
|
||||||
View для удаления документа перемещения.
|
View для удаления документа перемещения.
|
||||||
"""
|
"""
|
||||||
model = TransferBatch
|
model = TransferDocument
|
||||||
template_name = 'inventory/transfer/transfer_confirm_delete.html'
|
template_name = 'inventory/transfer/transfer_confirm_delete.html'
|
||||||
success_url = reverse_lazy('inventory:transfer-list')
|
success_url = reverse_lazy('inventory:transfer-list')
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
transfer_batch = self.get_object()
|
transfer_document = self.get_object()
|
||||||
messages.success(self.request, f'Документ перемещения {transfer_batch.document_number} удалён.')
|
messages.success(self.request, f'Документ перемещения {transfer_document.document_number} удалён.')
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user