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 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,

View File

@@ -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
)

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} за ед."
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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -534,8 +534,100 @@
</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;">
<strong>Примечание:</strong> Показаны последние 100 записей для каждой таблицы.
<strong>Примечание:</strong> Показаны последние 100 записей для каждой таблицы.
Используйте фильтры для уточнения результатов.
</div>
</div>

View File

@@ -239,16 +239,16 @@
</a>
</div>
<!-- Партии поступлений -->
<!-- Документы поступлений -->
<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="d-flex align-items-center">
<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>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 text-dark">Партии поступлений</h6>
<h6 class="mb-0 text-dark">Документы поступлений</h6>
<small class="text-muted">История поступлений</small>
</div>
<i class="bi bi-chevron-right text-muted"></i>

View File

@@ -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/<int:pk>/delete/', WarehouseDeleteView.as_view(), name='warehouse-delete'),
path('warehouses/<int:pk>/set-default/', SetDefaultWarehouseView.as_view(), name='warehouse-set-default'),
# ==================== INCOMING BATCH ====================
path('incoming-batches/', IncomingBatchListView.as_view(), name='incoming-batch-list'),
path('incoming-batches/<int:pk>/', IncomingBatchDetailView.as_view(), name='incoming-batch-detail'),
# ==================== INCOMING BATCH (УДАЛЕНО) ====================
# IncomingBatch и Incoming удалены. Используйте IncomingDocument вместо них.
# ==================== SALE ====================
path('sales/', SaleListView.as_view(), name='sale-list'),

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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,