Улучшения инвентаризации: автоматическое проведение документов, оптимизация запросов и улучшения UI

- Автоматическое проведение документов списания и оприходования после завершения инвентаризации
- Оптимизация SQL-запросов: устранение N+1, bulk-операции для Stock, агрегация для StockBatch и Reservation
- Изменение формулы расчета разницы: (quantity_fact + quantity_reserved) - quantity_available
- Переименование поля 'По факту' в 'Подсчитано (факт, свободные)'
- Добавлены столбцы 'В резервах' и 'Всего на складе' в таблицу инвентаризации
- Перемещение столбца 'В системе (свободно)' после 'В резервах' с визуальным выделением
- Центральное выравнивание значений в столбцах таблицы
- Автоматическое выделение текста при фокусе на поле ввода количества
- Исправление форматирования разницы (убраны лишние нули)
- Изменение статуса 'Не обработана' на 'Не проведено'
- Добавление номера документа для инвентаризаций (INV-XXXXXX)
- Отображение всех типов списаний в debug-странице (WriteOff, WriteOffDocument, WriteOffDocumentItem)
- Улучшение отображения документов в детальном просмотре инвентаризации с возможностью перехода к ним
This commit is contained in:
2025-12-21 23:59:02 +03:00
parent bb821f9ef4
commit a8ba5ce780
16 changed files with 1619 additions and 194 deletions

View File

@@ -327,6 +327,14 @@ class Inventory(models.Model):
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
related_name='inventories', verbose_name="Склад")
document_number = models.CharField(
max_length=100,
unique=True,
db_index=True,
blank=True,
null=True,
verbose_name="Номер документа"
)
date = models.DateTimeField(auto_now_add=True, verbose_name="Дата инвентаризации")
status = models.CharField(max_length=20, choices=STATUS_CHOICES,
default='draft', verbose_name="Статус")
@@ -338,8 +346,13 @@ class Inventory(models.Model):
verbose_name = "Инвентаризация"
verbose_name_plural = "Инвентаризации"
ordering = ['-date']
indexes = [
models.Index(fields=['document_number']),
]
def __str__(self):
if self.document_number:
return f"{self.document_number} - {self.warehouse.name} ({self.date.strftime('%Y-%m-%d')})"
return f"Инвентаризация {self.warehouse.name} ({self.date.strftime('%Y-%m-%d')})"
@@ -354,9 +367,11 @@ class InventoryLine(models.Model):
quantity_system = models.DecimalField(max_digits=10, decimal_places=3,
verbose_name="Количество в системе")
quantity_fact = models.DecimalField(max_digits=10, decimal_places=3,
verbose_name="Фактическое количество")
verbose_name="Подсчитано (факт, свободные)",
help_text="Количество свободных товаров, подсчитанных физически")
difference = models.DecimalField(max_digits=10, decimal_places=3,
default=0, verbose_name="Разница (факт - система)",
default=0, verbose_name="Итоговая разница",
help_text="(Подсчитано + Зарезервировано) - Всего на складе",
editable=False)
processed = models.BooleanField(default=False,
verbose_name="Обработана (создана операция)")
@@ -370,7 +385,32 @@ class InventoryLine(models.Model):
def save(self, *args, **kwargs):
# Автоматически рассчитываем разницу
self.difference = self.quantity_fact - self.quantity_system
# Формула: (quantity_fact + quantity_reserved) - quantity_available
# Где quantity_fact - подсчитанные свободные товары
# Для расчета нужны quantity_reserved и quantity_available из Stock
# Если они не переданы в kwargs, получаем из Stock
quantity_reserved = kwargs.pop('quantity_reserved', None)
quantity_available = kwargs.pop('quantity_available', None)
if quantity_reserved is None or quantity_available is None:
# Получаем из Stock для расчета
from inventory.models import Stock
stock = Stock.objects.filter(
product=self.product,
warehouse=self.inventory.warehouse
).first()
if stock:
stock.refresh_from_batches()
quantity_reserved = stock.quantity_reserved
quantity_available = stock.quantity_available
# Вычисляем разницу по новой формуле: (quantity_fact + quantity_reserved) - quantity_available
if quantity_reserved is not None and quantity_available is not None:
self.difference = (self.quantity_fact + quantity_reserved) - quantity_available
else:
# Fallback на старую формулу, если Stock недоступен (не должно происходить в нормальной работе)
self.difference = self.quantity_fact - self.quantity_system
super().save(*args, **kwargs)
@@ -762,6 +802,7 @@ class DocumentCounter(models.Model):
('transfer', 'Перемещение товара'),
('writeoff', 'Списание товара'),
('incoming', 'Поступление товара'),
('inventory', 'Инвентаризация'),
]
counter_type = models.CharField(
@@ -954,6 +995,16 @@ class WriteOffDocument(models.Model):
verbose_name="Примечания"
)
# Связь с инвентаризацией
inventory = models.ForeignKey(
'Inventory',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='writeoff_documents',
verbose_name="Инвентаризация"
)
# Аудит
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -1168,6 +1219,16 @@ class IncomingDocument(models.Model):
verbose_name="Примечания"
)
# Связь с инвентаризацией
inventory = models.ForeignKey(
'Inventory',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='incoming_documents',
verbose_name="Инвентаризация"
)
# Аудит
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,