From c534e27c41e6db887e00fc07310a0b667f3bdfe9 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Fri, 26 Dec 2025 19:55:50 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20Transfer=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Текущее состояние перед рефакторингом Transfer → TransferDocument. Все изменения с последнего коммита по улучшению системы поступлений. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- myproject/inventory/admin.py | 2 +- myproject/inventory/forms.py | 2 +- ...0004_remove_incoming_batch_and_incoming.py | 23 ++++ myproject/inventory/models.py | 98 ++------------ .../services/incoming_document_service.py | 42 +++--- myproject/inventory/signals.py | 128 +----------------- .../templates/inventory/debug_page.html | 94 ++++++++++++- .../inventory/templates/inventory/home.html | 6 +- myproject/inventory/urls.py | 7 +- .../inventory/utils/document_generator.py | 28 ++-- myproject/inventory/views/__init__.py | 4 +- myproject/inventory/views/batch.py | 40 +----- myproject/inventory/views/debug_views.py | 15 +- myproject/УЛУЧШИТЬ ПОСТУПЛЕНИЕ.md | 22 +-- 14 files changed, 198 insertions(+), 313 deletions(-) create mode 100644 myproject/inventory/migrations/0004_remove_incoming_batch_and_incoming.py diff --git a/myproject/inventory/admin.py b/myproject/inventory/admin.py index a340bee..ebb371a 100644 --- a/myproject/inventory/admin.py +++ b/myproject/inventory/admin.py @@ -5,7 +5,7 @@ from django.db.models import Sum from decimal import Decimal from inventory.models import ( - Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer, + Warehouse, StockBatch, Sale, WriteOff, Transfer, Inventory, InventoryLine, Reservation, Stock, StockMovement, SaleBatchAllocation, Showcase, WriteOffDocument, WriteOffDocumentItem, IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index 5d169e4..69c59c6 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from decimal import Decimal 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, IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput ) diff --git a/myproject/inventory/migrations/0004_remove_incoming_batch_and_incoming.py b/myproject/inventory/migrations/0004_remove_incoming_batch_and_incoming.py new file mode 100644 index 0000000..9b9a543 --- /dev/null +++ b/myproject/inventory/migrations/0004_remove_incoming_batch_and_incoming.py @@ -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', + ), + ] diff --git a/myproject/inventory/models.py b/myproject/inventory/models.py index 804880d..0b5d50f 100644 --- a/myproject/inventory/models.py +++ b/myproject/inventory/models.py @@ -100,84 +100,9 @@ class StockBatch(models.Model): return f"{self.product.name} на {self.warehouse.name} - Остаток: {self.quantity} шт @ {self.cost_price} за ед." -class IncomingBatch(models.Model): - """ - Партия поступления товара (один номер документа = одна партия). - Содержит один номер документа и может включать несколько товаров. - """ - 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 +# Модели IncomingBatch и Incoming удалены - заменены на IncomingDocument/IncomingDocumentItem +# Теперь используется упрощенная архитектура: +# IncomingDocument → IncomingDocumentItem → StockBatch (напрямую при проведении) class Sale(models.Model): @@ -1207,8 +1132,8 @@ class IncomingDocument(models.Model): Сценарий использования: 1. Создается черновик (draft) 2. В течение дня добавляются товары (IncomingDocumentItem) - 3. В конце смены документ проводится (confirmed) → создаются IncomingBatch и Incoming - 4. Сигнал автоматически создает StockBatch и обновляет Stock + 3. В конце смены документ проводится (confirmed) → создается StockBatch напрямую + 4. Stock автоматически обновляется """ STATUS_CHOICES = [ ('draft', 'Черновик'), @@ -1216,6 +1141,12 @@ class IncomingDocument(models.Model): ('cancelled', 'Отменён'), ] + RECEIPT_TYPE_CHOICES = [ + ('supplier', 'Поступление от поставщика'), + ('inventory', 'Оприходование при инвентаризации'), + ('adjustment', 'Оприходование без инвентаризации'), + ] + document_number = models.CharField( max_length=100, unique=True, @@ -1245,7 +1176,7 @@ class IncomingDocument(models.Model): receipt_type = models.CharField( max_length=20, - choices=IncomingBatch.RECEIPT_TYPE_CHOICES, + choices=RECEIPT_TYPE_CHOICES, default='supplier', db_index=True, verbose_name="Тип поступления" @@ -1355,9 +1286,8 @@ class IncomingDocumentItem(models.Model): - Резервирование НЕ создается (товар еще не поступил) При проведении документа: - 1. Создается IncomingBatch с номером документа - 2. Создается Incoming запись для каждого товара - 3. Сигнал create_stock_batch_on_incoming автоматически создает StockBatch + 1. Для каждой позиции напрямую создается StockBatch + 2. Stock автоматически обновляется """ document = models.ForeignKey( IncomingDocument, diff --git a/myproject/inventory/services/incoming_document_service.py b/myproject/inventory/services/incoming_document_service.py index 4ac7bf8..17ba1d7 100644 --- a/myproject/inventory/services/incoming_document_service.py +++ b/myproject/inventory/services/incoming_document_service.py @@ -14,7 +14,7 @@ from django.utils import timezone from django.core.exceptions import ValidationError from inventory.models import ( - IncomingDocument, IncomingDocumentItem, IncomingBatch, Incoming + IncomingDocument, IncomingDocumentItem, StockBatch, Stock ) from inventory.utils.document_generator import generate_incoming_document_number @@ -165,10 +165,9 @@ class IncomingDocumentService: Процесс: 1. Проверяем что документ - черновик и имеет позиции - 2. Создаем IncomingBatch с номером документа - 3. Для каждой позиции создаем Incoming запись - 4. Сигнал create_stock_batch_on_incoming автоматически создаст StockBatch - 5. Меняем статус документа на 'confirmed' + 2. Для каждой позиции создаем StockBatch напрямую + 3. Обновляем Stock + 4. Меняем статус документа на 'confirmed' Args: document: IncomingDocument @@ -188,30 +187,30 @@ class IncomingDocumentService: if not document.items.exists(): raise ValidationError("Нельзя провести пустой документ") - # Создаем IncomingBatch - incoming_batch = IncomingBatch.objects.create( - 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 = [] + # Создаем StockBatch напрямую для каждого товара + batches_created = [] total_cost = Decimal('0') for item in document.items.select_related('product'): - incoming = Incoming.objects.create( - batch=incoming_batch, + # Создаем партию товара на складе + stock_batch = StockBatch.objects.create( product=item.product, + warehouse=document.warehouse, quantity=item.quantity, 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 + # Обновляем или создаем запись в Stock + stock, _ = Stock.objects.get_or_create( + product=item.product, + warehouse=document.warehouse + ) + # Пересчитываем остаток из всех активных партий + stock.refresh_from_batches() + # Обновляем статус документа document.status = 'confirmed' document.confirmed_by = confirmed_by @@ -220,8 +219,7 @@ class IncomingDocumentService: return { 'document': document, - 'incoming_batch': incoming_batch, - 'incomings_created': len(incomings_created), + 'batches_created': len(batches_created), 'total_quantity': document.total_quantity, 'total_cost': total_cost } diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index c619720..f30f58d 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -14,7 +14,7 @@ from decimal import Decimal from django.core.exceptions import ValidationError 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.batch_manager import StockBatchManager # InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view @@ -1046,131 +1046,7 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs): ) -@receiver(post_save, sender=Incoming) -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 - ) +# Сигналы для Incoming удалены - теперь StockBatch создается напрямую в IncomingDocumentService @receiver(post_save, sender=Sale) diff --git a/myproject/inventory/templates/inventory/debug_page.html b/myproject/inventory/templates/inventory/debug_page.html index 547ea32..dd7bef9 100644 --- a/myproject/inventory/templates/inventory/debug_page.html +++ b/myproject/inventory/templates/inventory/debug_page.html @@ -534,8 +534,100 @@ + +
+

📥 Документы поступления IncomingDocument ({{ incoming_documents.count }})

+
+ + + + + + + + + + + + + + + + {% for doc in incoming_documents %} + + + + + + + + + + + + {% empty %} + + {% endfor %} + +
IDНомерСкладСтатусТипДатаПоставщикСоздалПровёл
{{ doc.id }}{{ doc.document_number }}{{ doc.warehouse.name }} + {% if doc.status == 'draft' %} + Черновик + {% elif doc.status == 'confirmed' %} + Проведён + {% elif doc.status == 'cancelled' %} + Отменён + {% else %} + {{ doc.status }} + {% endif %} + {{ doc.get_receipt_type_display }}{{ doc.date|date:"d.m.Y" }}{{ doc.supplier_name|default:"-" }}{{ doc.created_by.username|default:"-" }} + {% if doc.confirmed_by %} + {{ doc.confirmed_by.username }} ({{ doc.confirmed_at|date:"d.m H:i" }}) + {% else %} + - + {% endif %} +
Нет документов поступления
+
+
+ + +
+

📋 Строки документов поступления IncomingDocumentItem ({{ incoming_document_items.count }})

+
+ + + + + + + + + + + + + + + {% for item in incoming_document_items %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
IDДокументТоварСкладКол-воСебестоимостьСуммаПримечания
{{ item.id }}{{ item.document.document_number }}{{ item.product.name }} ({{ item.product.sku }}){{ item.document.warehouse.name }}{{ item.quantity }}{{ item.cost_price }} ₽{{ item.total_cost }} ₽{{ item.notes|default:"-" }}
Нет строк документов поступления
+
+
+
- Примечание: Показаны последние 100 записей для каждой таблицы. + Примечание: Показаны последние 100 записей для каждой таблицы. Используйте фильтры для уточнения результатов.
diff --git a/myproject/inventory/templates/inventory/home.html b/myproject/inventory/templates/inventory/home.html index cdaaafe..db5c0e5 100644 --- a/myproject/inventory/templates/inventory/home.html +++ b/myproject/inventory/templates/inventory/home.html @@ -239,16 +239,16 @@ - +
- +
-
Партии поступлений
+
Документы поступлений
История поступлений
diff --git a/myproject/inventory/urls.py b/myproject/inventory/urls.py index 338d0bc..fb9146d 100644 --- a/myproject/inventory/urls.py +++ b/myproject/inventory/urls.py @@ -3,8 +3,6 @@ from django.urls import path from .views import ( # Warehouse WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView, - # IncomingBatch - IncomingBatchListView, IncomingBatchDetailView, # Sale SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView, # Inventory @@ -64,9 +62,8 @@ urlpatterns = [ path('warehouses//delete/', WarehouseDeleteView.as_view(), name='warehouse-delete'), path('warehouses//set-default/', SetDefaultWarehouseView.as_view(), name='warehouse-set-default'), - # ==================== INCOMING BATCH ==================== - path('incoming-batches/', IncomingBatchListView.as_view(), name='incoming-batch-list'), - path('incoming-batches//', IncomingBatchDetailView.as_view(), name='incoming-batch-detail'), + # ==================== INCOMING BATCH (УДАЛЕНО) ==================== + # IncomingBatch и Incoming удалены. Используйте IncomingDocument вместо них. # ==================== SALE ==================== path('sales/', SaleListView.as_view(), name='sale-list'), diff --git a/myproject/inventory/utils/document_generator.py b/myproject/inventory/utils/document_generator.py index 37dabde..bd89ccd 100644 --- a/myproject/inventory/utils/document_generator.py +++ b/myproject/inventory/utils/document_generator.py @@ -74,40 +74,30 @@ def _initialize_incoming_counter_if_needed(): Вызывается только если счетчик равен 0 (не инициализирован). Thread-safe через select_for_update. """ - from inventory.models import IncomingBatch, IncomingDocument + from inventory.models import IncomingDocument from django.db import transaction - + # Быстрая проверка без блокировки - если счетчик существует и > 0, выходим if DocumentCounter.objects.filter( counter_type='incoming', current_value__gt=0 ).exists(): return - + # Только если счетчик не инициализирован - делаем полную проверку с блокировкой with transaction.atomic(): counter = DocumentCounter.objects.select_for_update().filter( counter_type='incoming' ).first() - + # Двойная проверка: возможно другой поток уже инициализировал if counter and counter.current_value > 0: return - - # Собираем все номера документов - all_numbers = [] - - # Номера из IncomingBatch - batch_numbers = IncomingBatch.objects.filter( + + # Собираем все номера документов из IncomingDocument + all_numbers = list(IncomingDocument.objects.filter( document_number__startswith='IN-' - ).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) + ).values_list('document_number', flat=True)) if all_numbers: # Извлекаем максимальный номер из всех форматов @@ -135,7 +125,7 @@ def generate_incoming_document_number(): Thread-safe через DocumentCounter. При первом использовании автоматически инициализирует DocumentCounter - максимальным номером из существующих документов (IncomingBatch и IncomingDocument). + максимальным номером из существующих документов IncomingDocument. Returns: str: Сгенерированный номер документа (например, IN-000001) diff --git a/myproject/inventory/views/__init__.py b/myproject/inventory/views/__init__.py index 0db6955..bcf2bca 100644 --- a/myproject/inventory/views/__init__.py +++ b/myproject/inventory/views/__init__.py @@ -19,7 +19,7 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required 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 .inventory_ops import ( InventoryListView, InventoryCreateView, InventoryDetailView, @@ -57,8 +57,6 @@ __all__ = [ 'inventory_home', # Warehouse 'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView', 'SetDefaultWarehouseView', - # IncomingBatch - 'IncomingBatchListView', 'IncomingBatchDetailView', # Sale 'SaleListView', 'SaleCreateView', 'SaleUpdateView', 'SaleDeleteView', 'SaleDetailView', # Inventory diff --git a/myproject/inventory/views/batch.py b/myproject/inventory/views/batch.py index ef49b33..dcc8a81 100644 --- a/myproject/inventory/views/batch.py +++ b/myproject/inventory/views/batch.py @@ -1,47 +1,13 @@ # -*- coding: utf-8 -*- """ Batch views - READ ONLY -- IncomingBatch (Партии поступлений) - StockBatch (Партии товара на складе) + +ПРИМЕЧАНИЕ: IncomingBatch и Incoming удалены. Используйте IncomingDocument вместо них. """ 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 +from ..models import StockBatch, SaleBatchAllocation, WriteOff class StockBatchListView(LoginRequiredMixin, ListView): diff --git a/myproject/inventory/views/debug_views.py b/myproject/inventory/views/debug_views.py index 455ca64..fee76a4 100644 --- a/myproject/inventory/views/debug_views.py +++ b/myproject/inventory/views/debug_views.py @@ -5,7 +5,10 @@ from django.contrib.auth.decorators import login_required, user_passes_test from django.shortcuts import render 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 products.models import Product from inventory.models import Warehouse @@ -44,6 +47,9 @@ def debug_inventory_page(request): writeoff_document_items = WriteOffDocumentItem.objects.select_related( 'product', 'document__warehouse' ).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') # Применяем фильтры @@ -56,6 +62,7 @@ def debug_inventory_page(request): allocations = allocations.filter(sale__product_id=product_id) writeoffs = writeoffs.filter(batch__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() else: product = None @@ -96,6 +103,8 @@ def debug_inventory_page(request): writeoffs = writeoffs.filter(batch__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) + incoming_documents = incoming_documents.filter(warehouse_id=warehouse_id) + incoming_document_items = incoming_document_items.filter(document__warehouse_id=warehouse_id) else: warehouse = None @@ -108,6 +117,8 @@ def debug_inventory_page(request): writeoffs = writeoffs[:100] writeoff_documents = writeoff_documents[:50] writeoff_document_items = writeoff_document_items[:100] + incoming_documents = incoming_documents[:50] + incoming_document_items = incoming_document_items[:100] orders = orders[:50] # Списки для фильтров @@ -123,6 +134,8 @@ def debug_inventory_page(request): 'writeoffs': writeoffs, 'writeoff_documents': writeoff_documents, 'writeoff_document_items': writeoff_document_items, + 'incoming_documents': incoming_documents, + 'incoming_document_items': incoming_document_items, 'orders': orders, 'products': products, 'warehouses': warehouses, diff --git a/myproject/УЛУЧШИТЬ ПОСТУПЛЕНИЕ.md b/myproject/УЛУЧШИТЬ ПОСТУПЛЕНИЕ.md index 91f966c..0299f64 100644 --- a/myproject/УЛУЧШИТЬ ПОСТУПЛЕНИЕ.md +++ b/myproject/УЛУЧШИТЬ ПОСТУПЛЕНИЕ.md @@ -56,11 +56,11 @@ def get_queryset(self): ## 🟡 Средний приоритет -### 4. Рефакторинг модельной избыточности -**Проблема:** IncomingDocument → IncomingBatch → Incoming создает 3 уровня данных -**Решение:** Долгосрочная миграция к упрощенной структуре +### 4. ✅ Рефакторинг модельной избыточности (ВЫПОЛНЕНО) +**Проблема:** IncomingDocument → IncomingBatch → Incoming создавало 3 уровня данных +**Решение:** ✅ Миграция к упрощенной структуре завершена -**Архитектура будущего:** +**Текущая архитектура:** ``` IncomingDocument (документ) ↓ @@ -69,12 +69,14 @@ IncomingDocumentItem (позиции документа) StockBatch (напрямую создается из items при подтверждении) ``` -**Преимущества:** -- Убрать промежуточные Incoming/IncomingBatch -- Упростить код сигналов -- Меньше JOIN'ов в запросах +**Достигнутые результаты:** +- ✅ Удалены промежуточные модели Incoming/IncomingBatch +- ✅ Упрощен код сигналов (удалены create_stock_batch_on_incoming и update_stock_batch_on_incoming_edit) +- ✅ Упрощен IncomingDocumentService.confirm_document() - напрямую создает StockBatch +- ✅ Меньше JOIN'ов в запросах +- ✅ Применены миграции БД для удаления таблиц -**Миграция:** Постепенная, требует переписывания signals и services +**Дата выполнения:** 2025-12-26 --- @@ -135,7 +137,7 @@ def add_items_bulk(document, items_data): 1. ✅ **Неделя 1:** Безопасность (права доступ, п.1) 2. ✅ **Неделя 2:** Тесты (критические пути, п.2) 3. ✅ **Неделя 3:** Производительность (N+1, п.3) -4. 📅 **Квартал 2:** Рефакторинг моделей (п.4) +4. ✅ **26.12.2025:** Рефакторинг моделей (п.4) - избавились от лишних сущностей 5. 📅 **По необходимости:** Bulk операции (п.5), документация (п.6) ---