refactor: подготовка к стандартизации Transfer моделей

Текущее состояние перед рефакторингом Transfer → 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 19:55:50 +03:00
parent 0da2995a74
commit c534e27c41
14 changed files with 198 additions and 313 deletions

View File

@@ -5,7 +5,7 @@ from django.db.models import Sum
from decimal import Decimal from decimal import Decimal
from inventory.models import ( from inventory.models import (
Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer, Warehouse, StockBatch, Sale, WriteOff, Transfer,
Inventory, InventoryLine, Reservation, Stock, StockMovement, Inventory, InventoryLine, Reservation, Stock, StockMovement,
SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem, SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem,
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput,

View File

@@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from decimal import Decimal from decimal import Decimal
from .models import ( from .models import (
Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch, Warehouse, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock, TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput
) )

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.10 on 2025-12-26 14:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_alter_documentcounter_counter_type_and_more'),
]
operations = [
migrations.RemoveField(
model_name='incomingbatch',
name='warehouse',
),
migrations.DeleteModel(
name='Incoming',
),
migrations.DeleteModel(
name='IncomingBatch',
),
]

View File

@@ -100,84 +100,9 @@ class StockBatch(models.Model):
return f"{self.product.name} на {self.warehouse.name} - Остаток: {self.quantity} шт @ {self.cost_price} за ед." return f"{self.product.name} на {self.warehouse.name} - Остаток: {self.quantity} шт @ {self.cost_price} за ед."
class IncomingBatch(models.Model): # Модели IncomingBatch и Incoming удалены - заменены на IncomingDocument/IncomingDocumentItem
""" # Теперь используется упрощенная архитектура:
Партия поступления товара (один номер документа = одна партия). # IncomingDocument → IncomingDocumentItem → StockBatch (напрямую при проведении)
Содержит один номер документа и может включать несколько товаров.
"""
RECEIPT_TYPE_CHOICES = [
('supplier', 'Поступление от поставщика'),
('inventory', 'Оприходование при инвентаризации'),
('adjustment', 'Оприходование без инвентаризации'),
]
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
related_name='incoming_batches', verbose_name="Склад")
document_number = models.CharField(max_length=100, unique=True, db_index=True,
verbose_name="Номер документа")
receipt_type = models.CharField(
max_length=20,
choices=RECEIPT_TYPE_CHOICES,
default='supplier',
db_index=True,
verbose_name="Тип поступления"
)
supplier_name = models.CharField(max_length=200, blank=True, null=True,
verbose_name="Наименование поставщика")
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
class Meta:
verbose_name = "Партия поступления"
verbose_name_plural = "Партии поступлений"
ordering = ['-created_at']
indexes = [
models.Index(fields=['document_number']),
models.Index(fields=['warehouse']),
models.Index(fields=['receipt_type']),
models.Index(fields=['-created_at']),
]
def __str__(self):
total_items = self.items.count()
total_qty = self.items.aggregate(models.Sum('quantity'))['quantity__sum'] or 0
return f"Партия {self.document_number}: {total_items} товаров, {total_qty} шт"
class Incoming(models.Model):
"""
Товар в партии поступления. Много товаров = одна партия (IncomingBatch).
"""
batch = models.ForeignKey(IncomingBatch, on_delete=models.CASCADE,
related_name='items', verbose_name="Партия")
product = models.ForeignKey(Product, on_delete=models.CASCADE,
related_name='incomings', verbose_name="Товар")
quantity = models.DecimalField(max_digits=10, decimal_places=3, verbose_name="Количество")
cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Закупочная цена")
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
stock_batch = models.ForeignKey(StockBatch, on_delete=models.SET_NULL, null=True, blank=True,
related_name='incomings', verbose_name="Складская партия")
class Meta:
verbose_name = "Товар в поступлении"
verbose_name_plural = "Товары в поступлениях"
ordering = ['-created_at']
indexes = [
models.Index(fields=['batch']),
models.Index(fields=['product']),
models.Index(fields=['-created_at']),
]
unique_together = [['batch', 'product']] # Один товар максимум один раз в партии
def __str__(self):
return f"{self.product.name}: {self.quantity} шт (партия {self.batch.document_number})"
@property
def can_edit(self):
"""Можно ли редактировать приход"""
return self.stock_batch is None
class Sale(models.Model): class Sale(models.Model):
@@ -1207,8 +1132,8 @@ class IncomingDocument(models.Model):
Сценарий использования: Сценарий использования:
1. Создается черновик (draft) 1. Создается черновик (draft)
2. В течение дня добавляются товары (IncomingDocumentItem) 2. В течение дня добавляются товары (IncomingDocumentItem)
3. В конце смены документ проводится (confirmed) → создаются IncomingBatch и Incoming 3. В конце смены документ проводится (confirmed) → создается StockBatch напрямую
4. Сигнал автоматически создает StockBatch и обновляет Stock 4. Stock автоматически обновляется
""" """
STATUS_CHOICES = [ STATUS_CHOICES = [
('draft', 'Черновик'), ('draft', 'Черновик'),
@@ -1216,6 +1141,12 @@ class IncomingDocument(models.Model):
('cancelled', 'Отменён'), ('cancelled', 'Отменён'),
] ]
RECEIPT_TYPE_CHOICES = [
('supplier', 'Поступление от поставщика'),
('inventory', 'Оприходование при инвентаризации'),
('adjustment', 'Оприходование без инвентаризации'),
]
document_number = models.CharField( document_number = models.CharField(
max_length=100, max_length=100,
unique=True, unique=True,
@@ -1245,7 +1176,7 @@ class IncomingDocument(models.Model):
receipt_type = models.CharField( receipt_type = models.CharField(
max_length=20, max_length=20,
choices=IncomingBatch.RECEIPT_TYPE_CHOICES, choices=RECEIPT_TYPE_CHOICES,
default='supplier', default='supplier',
db_index=True, db_index=True,
verbose_name="Тип поступления" verbose_name="Тип поступления"
@@ -1355,9 +1286,8 @@ class IncomingDocumentItem(models.Model):
- Резервирование НЕ создается (товар еще не поступил) - Резервирование НЕ создается (товар еще не поступил)
При проведении документа: При проведении документа:
1. Создается IncomingBatch с номером документа 1. Для каждой позиции напрямую создается StockBatch
2. Создается Incoming запись для каждого товара 2. Stock автоматически обновляется
3. Сигнал create_stock_batch_on_incoming автоматически создает StockBatch
""" """
document = models.ForeignKey( document = models.ForeignKey(
IncomingDocument, IncomingDocument,

View File

@@ -14,7 +14,7 @@ from django.utils import timezone
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from inventory.models import ( from inventory.models import (
IncomingDocument, IncomingDocumentItem, IncomingBatch, Incoming IncomingDocument, IncomingDocumentItem, StockBatch, Stock
) )
from inventory.utils.document_generator import generate_incoming_document_number from inventory.utils.document_generator import generate_incoming_document_number
@@ -165,10 +165,9 @@ class IncomingDocumentService:
Процесс: Процесс:
1. Проверяем что документ - черновик и имеет позиции 1. Проверяем что документ - черновик и имеет позиции
2. Создаем IncomingBatch с номером документа 2. Для каждой позиции создаем StockBatch напрямую
3. Для каждой позиции создаем Incoming запись 3. Обновляем Stock
4. Сигнал create_stock_batch_on_incoming автоматически создаст StockBatch 4. Меняем статус документа на 'confirmed'
5. Меняем статус документа на 'confirmed'
Args: Args:
document: IncomingDocument document: IncomingDocument
@@ -188,30 +187,30 @@ class IncomingDocumentService:
if not document.items.exists(): if not document.items.exists():
raise ValidationError("Нельзя провести пустой документ") raise ValidationError("Нельзя провести пустой документ")
# Создаем IncomingBatch # Создаем StockBatch напрямую для каждого товара
incoming_batch = IncomingBatch.objects.create( batches_created = []
warehouse=document.warehouse,
document_number=document.document_number,
receipt_type=document.receipt_type,
supplier_name=document.supplier_name if document.receipt_type == 'supplier' else '',
notes=document.notes
)
# Создаем Incoming записи для каждого товара
incomings_created = []
total_cost = Decimal('0') total_cost = Decimal('0')
for item in document.items.select_related('product'): for item in document.items.select_related('product'):
incoming = Incoming.objects.create( # Создаем партию товара на складе
batch=incoming_batch, stock_batch = StockBatch.objects.create(
product=item.product, product=item.product,
warehouse=document.warehouse,
quantity=item.quantity, quantity=item.quantity,
cost_price=item.cost_price, cost_price=item.cost_price,
notes=item.notes is_active=True
) )
incomings_created.append(incoming) batches_created.append(stock_batch)
total_cost += item.total_cost total_cost += item.total_cost
# Обновляем или создаем запись в Stock
stock, _ = Stock.objects.get_or_create(
product=item.product,
warehouse=document.warehouse
)
# Пересчитываем остаток из всех активных партий
stock.refresh_from_batches()
# Обновляем статус документа # Обновляем статус документа
document.status = 'confirmed' document.status = 'confirmed'
document.confirmed_by = confirmed_by document.confirmed_by = confirmed_by
@@ -220,8 +219,7 @@ class IncomingDocumentService:
return { return {
'document': document, 'document': document,
'incoming_batch': incoming_batch, 'batches_created': len(batches_created),
'incomings_created': len(incomings_created),
'total_quantity': document.total_quantity, 'total_quantity': document.total_quantity,
'total_cost': total_cost 'total_cost': total_cost
} }

View File

@@ -14,7 +14,7 @@ from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from orders.models import Order, OrderItem from orders.models import Order, OrderItem
from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem, Transformation, TransformationInput, TransformationOutput from inventory.models import Reservation, Warehouse, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem, Transformation, TransformationInput, TransformationOutput
from inventory.services import SaleProcessor from inventory.services import SaleProcessor
from inventory.services.batch_manager import StockBatchManager from inventory.services.batch_manager import StockBatchManager
# InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view # InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view
@@ -1046,131 +1046,7 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
) )
@receiver(post_save, sender=Incoming) # Сигналы для Incoming удалены - теперь StockBatch создается напрямую в IncomingDocumentService
def create_stock_batch_on_incoming(sender, instance, created, **kwargs):
"""
Сигнал: При создании товара в приходе (Incoming) автоматически создается StockBatch и обновляется Stock.
Архитектура:
- IncomingBatch: одна партия поступления (IN-0000-0001) содержит несколько товаров
- Incoming: один товар в партии поступления
- StockBatch: одна партия товара на складе (создается для каждого товара в приходе)
Для FIFO: каждый товар имеет свою partия, чтобы можно было списывать отдельно
Процесс:
1. Проверяем, новый ли товар в приходе
2. Если stock_batch еще не создан - создаем StockBatch для этого товара
3. Связываем Incoming с созданной StockBatch
4. Обновляем остатки на складе (Stock)
"""
if not created:
return # Только для новых приходов
# Если stock_batch уже установлен - не создаем новый
if instance.stock_batch:
return
# Получаем данные из партии поступления
incoming_batch = instance.batch
warehouse = incoming_batch.warehouse
# Создаем новую партию товара на складе
# Каждый товар в партии поступления → отдельная StockBatch
stock_batch = StockBatch.objects.create(
product=instance.product,
warehouse=warehouse,
quantity=instance.quantity,
cost_price=instance.cost_price,
is_active=True
)
# Связываем Incoming с созданной StockBatch
instance.stock_batch = stock_batch
instance.save(update_fields=['stock_batch'])
# Обновляем или создаем запись в Stock
stock, created_stock = Stock.objects.get_or_create(
product=instance.product,
warehouse=warehouse
)
# Пересчитываем остаток из всех активных партий
# refresh_from_batches() уже вызывает save(), поэтому не вызываем ещё раз
stock.refresh_from_batches()
@receiver(post_save, sender=Incoming)
def update_stock_batch_on_incoming_edit(sender, instance, created, **kwargs):
"""
Сигнал: При редактировании товара в приходе (Incoming) автоматически
обновляется связанная партия товара на складе (StockBatch).
Это обеспечивает синхронизацию данных между Incoming и StockBatch.
Архитектура:
- Если Incoming редактируется - обновляем StockBatch с новыми значениями
- Обновление StockBatch автоматически пересчитывает себестоимость товара (Product.cost_price)
через сигнал update_product_cost_on_batch_change()
Процесс:
1. Проверяем, это редактирование (created=False), а не создание
2. Получаем связанный StockBatch
3. Проверяем, изменились ли quantity или cost_price
4. Если да - обновляем StockBatch
5. Сохраняем StockBatch (запускает цепь пересчета себестоимости)
6. Обновляем остатки на складе (Stock)
"""
if created:
return # Только для редактирования (не для создания)
# Получаем связанный StockBatch
if not instance.stock_batch:
return # Если нет связи со StockBatch - нечего обновлять
stock_batch = instance.stock_batch
import logging
logger = logging.getLogger(__name__)
try:
# Проверяем, отличаются ли значения в StockBatch от Incoming
# Это говорит нам о том, что произошло редактирование
needs_update = (
stock_batch.quantity != instance.quantity or
stock_batch.cost_price != instance.cost_price
)
if not needs_update:
return # Никаких изменений
# Обновляем StockBatch с новыми значениями из Incoming
stock_batch.quantity = instance.quantity
stock_batch.cost_price = instance.cost_price
stock_batch.save()
logger.info(
f"✓ StockBatch #{stock_batch.id} обновлён при редактировании Incoming: "
f"quantity={instance.quantity}, cost_price={instance.cost_price} "
f"(товар: {instance.product.sku})"
)
# Обновляем Stock (остатки на складе)
warehouse = stock_batch.warehouse
stock, _ = Stock.objects.get_or_create(
product=instance.product,
warehouse=warehouse
)
stock.refresh_from_batches()
logger.info(
f"✓ Stock обновлён для товара {instance.product.sku} "
f"на складе {warehouse.name}"
)
except Exception as e:
logger.error(
f"Ошибка при обновлении StockBatch при редактировании Incoming #{instance.id}: {e}",
exc_info=True
)
@receiver(post_save, sender=Sale) @receiver(post_save, sender=Sale)

View File

@@ -534,6 +534,98 @@
</div> </div>
</div> </div>
<!-- ДОКУМЕНТЫ ПОСТУПЛЕНИЯ (IncomingDocument) -->
<div class="section-card">
<h3>📥 Документы поступления IncomingDocument ({{ incoming_documents.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Номер</th>
<th>Склад</th>
<th>Статус</th>
<th>Тип</th>
<th>Дата</th>
<th>Поставщик</th>
<th>Создал</th>
<th>Провёл</th>
</tr>
</thead>
<tbody>
{% for doc in incoming_documents %}
<tr>
<td>{{ doc.id }}</td>
<td><strong>{{ doc.document_number }}</strong></td>
<td>{{ doc.warehouse.name }}</td>
<td>
{% if doc.status == 'draft' %}
<span class="badge bg-warning text-dark">Черновик</span>
{% elif doc.status == 'confirmed' %}
<span class="badge bg-success">Проведён</span>
{% elif doc.status == 'cancelled' %}
<span class="badge bg-danger">Отменён</span>
{% else %}
<span class="badge bg-secondary">{{ doc.status }}</span>
{% endif %}
</td>
<td><span class="badge bg-info">{{ doc.get_receipt_type_display }}</span></td>
<td class="text-muted-small">{{ doc.date|date:"d.m.Y" }}</td>
<td>{{ doc.supplier_name|default:"-" }}</td>
<td class="text-muted-small">{{ doc.created_by.username|default:"-" }}</td>
<td class="text-muted-small">
{% if doc.confirmed_by %}
{{ doc.confirmed_by.username }} ({{ doc.confirmed_at|date:"d.m H:i" }})
{% else %}
-
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="9" class="text-center text-muted">Нет документов поступления</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- СТРОКИ ДОКУМЕНТОВ ПОСТУПЛЕНИЯ (IncomingDocumentItem) -->
<div class="section-card">
<h3>📋 Строки документов поступления IncomingDocumentItem ({{ incoming_document_items.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Документ</th>
<th>Товар</th>
<th>Склад</th>
<th>Кол-во</th>
<th>Себестоимость</th>
<th>Сумма</th>
<th>Примечания</th>
</tr>
</thead>
<tbody>
{% for item in incoming_document_items %}
<tr>
<td>{{ item.id }}</td>
<td><strong>{{ item.document.document_number }}</strong></td>
<td><strong>{{ item.product.name }}</strong> ({{ item.product.sku }})</td>
<td>{{ item.document.warehouse.name }}</td>
<td><span class="badge bg-success">{{ item.quantity }}</span></td>
<td>{{ item.cost_price }} ₽</td>
<td><strong>{{ item.total_cost }} ₽</strong></td>
<td class="text-muted-small">{{ item.notes|default:"-" }}</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center text-muted">Нет строк документов поступления</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="alert alert-secondary py-2 mt-3" style="font-size: 10px;"> <div class="alert alert-secondary py-2 mt-3" style="font-size: 10px;">
<strong>Примечание:</strong> Показаны последние 100 записей для каждой таблицы. <strong>Примечание:</strong> Показаны последние 100 записей для каждой таблицы.
Используйте фильтры для уточнения результатов. Используйте фильтры для уточнения результатов.

View File

@@ -239,16 +239,16 @@
</a> </a>
</div> </div>
<!-- Партии поступлений --> <!-- Документы поступлений -->
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<a href="{% url 'inventory:incoming-batch-list' %}" class="card shadow-sm h-100 text-decoration-none"> <a href="{% url 'inventory:incoming-list' %}" class="card shadow-sm h-100 text-decoration-none">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="rounded-circle bg-secondary bg-opacity-10 p-3 me-3"> <div class="rounded-circle bg-secondary bg-opacity-10 p-3 me-3">
<i class="bi bi-box-arrow-in-down text-secondary" style="font-size: 1.5rem;"></i> <i class="bi bi-box-arrow-in-down text-secondary" style="font-size: 1.5rem;"></i>
</div> </div>
<div class="flex-grow-1"> <div class="flex-grow-1">
<h6 class="mb-0 text-dark">Партии поступлений</h6> <h6 class="mb-0 text-dark">Документы поступлений</h6>
<small class="text-muted">История поступлений</small> <small class="text-muted">История поступлений</small>
</div> </div>
<i class="bi bi-chevron-right text-muted"></i> <i class="bi bi-chevron-right text-muted"></i>

View File

@@ -3,8 +3,6 @@ from django.urls import path
from .views import ( from .views import (
# Warehouse # Warehouse
WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView, WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView,
# IncomingBatch
IncomingBatchListView, IncomingBatchDetailView,
# Sale # Sale
SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView, SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView,
# Inventory # Inventory
@@ -64,9 +62,8 @@ urlpatterns = [
path('warehouses/<int:pk>/delete/', WarehouseDeleteView.as_view(), name='warehouse-delete'), path('warehouses/<int:pk>/delete/', WarehouseDeleteView.as_view(), name='warehouse-delete'),
path('warehouses/<int:pk>/set-default/', SetDefaultWarehouseView.as_view(), name='warehouse-set-default'), path('warehouses/<int:pk>/set-default/', SetDefaultWarehouseView.as_view(), name='warehouse-set-default'),
# ==================== INCOMING BATCH ==================== # ==================== INCOMING BATCH (УДАЛЕНО) ====================
path('incoming-batches/', IncomingBatchListView.as_view(), name='incoming-batch-list'), # IncomingBatch и Incoming удалены. Используйте IncomingDocument вместо них.
path('incoming-batches/<int:pk>/', IncomingBatchDetailView.as_view(), name='incoming-batch-detail'),
# ==================== SALE ==================== # ==================== SALE ====================
path('sales/', SaleListView.as_view(), name='sale-list'), path('sales/', SaleListView.as_view(), name='sale-list'),

View File

@@ -74,7 +74,7 @@ def _initialize_incoming_counter_if_needed():
Вызывается только если счетчик равен 0 (не инициализирован). Вызывается только если счетчик равен 0 (не инициализирован).
Thread-safe через select_for_update. Thread-safe через select_for_update.
""" """
from inventory.models import IncomingBatch, IncomingDocument from inventory.models import IncomingDocument
from django.db import transaction from django.db import transaction
# Быстрая проверка без блокировки - если счетчик существует и > 0, выходим # Быстрая проверка без блокировки - если счетчик существует и > 0, выходим
@@ -94,20 +94,10 @@ def _initialize_incoming_counter_if_needed():
if counter and counter.current_value > 0: if counter and counter.current_value > 0:
return return
# Собираем все номера документов # Собираем все номера документов из IncomingDocument
all_numbers = [] all_numbers = list(IncomingDocument.objects.filter(
# Номера из IncomingBatch
batch_numbers = IncomingBatch.objects.filter(
document_number__startswith='IN-' document_number__startswith='IN-'
).values_list('document_number', flat=True) ).values_list('document_number', flat=True))
all_numbers.extend(batch_numbers)
# Номера из IncomingDocument
doc_numbers = IncomingDocument.objects.filter(
document_number__startswith='IN-'
).values_list('document_number', flat=True)
all_numbers.extend(doc_numbers)
if all_numbers: if all_numbers:
# Извлекаем максимальный номер из всех форматов # Извлекаем максимальный номер из всех форматов
@@ -135,7 +125,7 @@ def generate_incoming_document_number():
Thread-safe через DocumentCounter. Thread-safe через DocumentCounter.
При первом использовании автоматически инициализирует DocumentCounter При первом использовании автоматически инициализирует DocumentCounter
максимальным номером из существующих документов (IncomingBatch и IncomingDocument). максимальным номером из существующих документов IncomingDocument.
Returns: Returns:
str: Сгенерированный номер документа (например, IN-000001) str: Сгенерированный номер документа (например, IN-000001)

View File

@@ -19,7 +19,7 @@ from django.shortcuts import render
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from .warehouse import WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView from .warehouse import WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView
from .batch import IncomingBatchListView, IncomingBatchDetailView, StockBatchListView, StockBatchDetailView from .batch import StockBatchListView, StockBatchDetailView
from .sale import SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView from .sale import SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView
from .inventory_ops import ( from .inventory_ops import (
InventoryListView, InventoryCreateView, InventoryDetailView, InventoryListView, InventoryCreateView, InventoryDetailView,
@@ -57,8 +57,6 @@ __all__ = [
'inventory_home', 'inventory_home',
# Warehouse # Warehouse
'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView', 'SetDefaultWarehouseView', 'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView', 'SetDefaultWarehouseView',
# IncomingBatch
'IncomingBatchListView', 'IncomingBatchDetailView',
# Sale # Sale
'SaleListView', 'SaleCreateView', 'SaleUpdateView', 'SaleDeleteView', 'SaleDetailView', 'SaleListView', 'SaleCreateView', 'SaleUpdateView', 'SaleDeleteView', 'SaleDetailView',
# Inventory # Inventory

View File

@@ -1,47 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Batch views - READ ONLY Batch views - READ ONLY
- IncomingBatch (Партии поступлений)
- StockBatch (Партии товара на складе) - StockBatch (Партии товара на складе)
ПРИМЕЧАНИЕ: IncomingBatch и Incoming удалены. Используйте IncomingDocument вместо них.
""" """
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import IncomingBatch, Incoming, StockBatch, SaleBatchAllocation, WriteOff from ..models import 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): class StockBatchListView(LoginRequiredMixin, ListView):

View File

@@ -5,7 +5,10 @@
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import render from django.shortcuts import render
from django.db.models import Q, Sum, Count from django.db.models import Q, Sum, Count
from inventory.models import StockBatch, Stock, Reservation, Sale, SaleBatchAllocation, WriteOff, WriteOffDocument, WriteOffDocumentItem from inventory.models import (
StockBatch, Stock, Reservation, Sale, SaleBatchAllocation, WriteOff,
WriteOffDocument, WriteOffDocumentItem, IncomingDocument, IncomingDocumentItem
)
from orders.models import Order from orders.models import Order
from products.models import Product from products.models import Product
from inventory.models import Warehouse from inventory.models import Warehouse
@@ -44,6 +47,9 @@ def debug_inventory_page(request):
writeoff_document_items = WriteOffDocumentItem.objects.select_related( writeoff_document_items = WriteOffDocumentItem.objects.select_related(
'product', 'document__warehouse' 'product', 'document__warehouse'
).order_by('-id') ).order_by('-id')
# Документы поступления
incoming_documents = IncomingDocument.objects.select_related('warehouse', 'created_by', 'confirmed_by').order_by('-date', '-created_at')
incoming_document_items = IncomingDocumentItem.objects.select_related('product', 'document__warehouse').order_by('-id')
orders = Order.objects.prefetch_related('items').order_by('-created_at') orders = Order.objects.prefetch_related('items').order_by('-created_at')
# Применяем фильтры # Применяем фильтры
@@ -56,6 +62,7 @@ def debug_inventory_page(request):
allocations = allocations.filter(sale__product_id=product_id) allocations = allocations.filter(sale__product_id=product_id)
writeoffs = writeoffs.filter(batch__product_id=product_id) writeoffs = writeoffs.filter(batch__product_id=product_id)
writeoff_document_items = writeoff_document_items.filter(product_id=product_id) writeoff_document_items = writeoff_document_items.filter(product_id=product_id)
incoming_document_items = incoming_document_items.filter(product_id=product_id)
orders = orders.filter(items__product_id=product_id).distinct() orders = orders.filter(items__product_id=product_id).distinct()
else: else:
product = None product = None
@@ -96,6 +103,8 @@ def debug_inventory_page(request):
writeoffs = writeoffs.filter(batch__warehouse_id=warehouse_id) writeoffs = writeoffs.filter(batch__warehouse_id=warehouse_id)
writeoff_documents = writeoff_documents.filter(warehouse_id=warehouse_id) writeoff_documents = writeoff_documents.filter(warehouse_id=warehouse_id)
writeoff_document_items = writeoff_document_items.filter(document__warehouse_id=warehouse_id) writeoff_document_items = writeoff_document_items.filter(document__warehouse_id=warehouse_id)
incoming_documents = incoming_documents.filter(warehouse_id=warehouse_id)
incoming_document_items = incoming_document_items.filter(document__warehouse_id=warehouse_id)
else: else:
warehouse = None warehouse = None
@@ -108,6 +117,8 @@ def debug_inventory_page(request):
writeoffs = writeoffs[:100] writeoffs = writeoffs[:100]
writeoff_documents = writeoff_documents[:50] writeoff_documents = writeoff_documents[:50]
writeoff_document_items = writeoff_document_items[:100] writeoff_document_items = writeoff_document_items[:100]
incoming_documents = incoming_documents[:50]
incoming_document_items = incoming_document_items[:100]
orders = orders[:50] orders = orders[:50]
# Списки для фильтров # Списки для фильтров
@@ -123,6 +134,8 @@ def debug_inventory_page(request):
'writeoffs': writeoffs, 'writeoffs': writeoffs,
'writeoff_documents': writeoff_documents, 'writeoff_documents': writeoff_documents,
'writeoff_document_items': writeoff_document_items, 'writeoff_document_items': writeoff_document_items,
'incoming_documents': incoming_documents,
'incoming_document_items': incoming_document_items,
'orders': orders, 'orders': orders,
'products': products, 'products': products,
'warehouses': warehouses, 'warehouses': warehouses,

View File

@@ -56,11 +56,11 @@ def get_queryset(self):
## 🟡 Средний приоритет ## 🟡 Средний приоритет
### 4. Рефакторинг модельной избыточности ### 4. Рефакторинг модельной избыточности (ВЫПОЛНЕНО)
**Проблема:** IncomingDocument → IncomingBatch → Incoming создает 3 уровня данных **Проблема:** IncomingDocument → IncomingBatch → Incoming создавало 3 уровня данных
**Решение:** Долгосрочная миграция к упрощенной структуре **Решение:** Миграция к упрощенной структуре завершена
**Архитектура будущего:** **Текущая архитектура:**
``` ```
IncomingDocument (документ) IncomingDocument (документ)
@@ -69,12 +69,14 @@ IncomingDocumentItem (позиции документа)
StockBatch (напрямую создается из items при подтверждении) StockBatch (напрямую создается из items при подтверждении)
``` ```
**Преимущества:** **Достигнутые результаты:**
- Убрать промежуточные Incoming/IncomingBatch - ✅ Удалены промежуточные модели Incoming/IncomingBatch
- Упростить код сигналов - Упрощен код сигналов (удалены create_stock_batch_on_incoming и update_stock_batch_on_incoming_edit)
- Меньше JOIN'ов в запросах - ✅ Упрощен IncomingDocumentService.confirm_document() - напрямую создает StockBatch
- ✅ Меньше JOIN'ов в запросах
- ✅ Применены миграции БД для удаления таблиц
**Миграция:** Постепенная, требует переписывания signals и services **Дата выполнения:** 2025-12-26
--- ---
@@ -135,7 +137,7 @@ def add_items_bulk(document, items_data):
1.**Неделя 1:** Безопасность (права доступ, п.1) 1.**Неделя 1:** Безопасность (права доступ, п.1)
2.**Неделя 2:** Тесты (критические пути, п.2) 2.**Неделя 2:** Тесты (критические пути, п.2)
3.**Неделя 3:** Производительность (N+1, п.3) 3.**Неделя 3:** Производительность (N+1, п.3)
4. 📅 **Квартал 2:** Рефакторинг моделей (п.4) 4. **26.12.2025:** Рефакторинг моделей (п.4) - избавились от лишних сущностей
5. 📅 **По необходимости:** Bulk операции (п.5), документация (п.6) 5. 📅 **По необходимости:** Bulk операции (п.5), документация (п.6)
--- ---