Улучшения инвентаризации: автоматическое проведение документов, оптимизация запросов и улучшения 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:
@@ -2,8 +2,8 @@
|
||||
Процессор для обработки инвентаризации.
|
||||
Основной функционал:
|
||||
- Обработка расхождений между фактом и системой
|
||||
- Автоматическое создание WriteOff для недостач (по FIFO)
|
||||
- Автоматическое создание Incoming для излишков
|
||||
- Автоматическое создание WriteOffDocument для недостач (черновик)
|
||||
- Автоматическое создание IncomingDocument для излишков (черновик)
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
@@ -11,11 +11,11 @@ from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from inventory.models import (
|
||||
Inventory, InventoryLine, WriteOff, Incoming, IncomingBatch,
|
||||
StockBatch, Stock
|
||||
Inventory, InventoryLine, StockBatch, Stock
|
||||
)
|
||||
from inventory.services.batch_manager import StockBatchManager
|
||||
from inventory.utils import generate_incoming_document_number
|
||||
from inventory.services.writeoff_document_service import WriteOffDocumentService
|
||||
from inventory.services.incoming_document_service import IncomingDocumentService
|
||||
|
||||
|
||||
class InventoryProcessor:
|
||||
@@ -28,9 +28,10 @@ class InventoryProcessor:
|
||||
def process_inventory(inventory_id):
|
||||
"""
|
||||
Обработать инвентаризацию:
|
||||
- Для недостач (разница < 0): создать WriteOff по FIFO
|
||||
- Для излишков (разница > 0): создать Incoming с новой партией
|
||||
- Для недостач (разница < 0): создать WriteOffDocument (черновик) с позициями
|
||||
- Для излишков (разница > 0): создать IncomingDocument (черновик) с позициями
|
||||
- Обновить статус inventory и lines
|
||||
- НЕ проводить документы сразу - они остаются в статусе 'draft'
|
||||
|
||||
Args:
|
||||
inventory_id: ID объекта Inventory
|
||||
@@ -39,34 +40,31 @@ class InventoryProcessor:
|
||||
dict: {
|
||||
'inventory': Inventory,
|
||||
'processed_lines': int,
|
||||
'writeoffs_created': int,
|
||||
'incomings_created': int,
|
||||
'writeoff_document': WriteOffDocument или None,
|
||||
'incoming_document': IncomingDocument или None,
|
||||
'errors': [...]
|
||||
}
|
||||
"""
|
||||
inventory = Inventory.objects.get(id=inventory_id)
|
||||
lines = InventoryLine.objects.filter(inventory=inventory, processed=False)
|
||||
|
||||
writeoffs_created = 0
|
||||
incomings_created = 0
|
||||
errors = []
|
||||
writeoff_document = None
|
||||
incoming_document = None
|
||||
|
||||
# Собираем недостачи и излишки
|
||||
deficit_lines = []
|
||||
surplus_lines = []
|
||||
|
||||
try:
|
||||
for line in lines:
|
||||
try:
|
||||
if line.difference < 0:
|
||||
# Недостача: списать по FIFO
|
||||
InventoryProcessor._create_writeoff_for_deficit(
|
||||
inventory, line
|
||||
)
|
||||
writeoffs_created += 1
|
||||
|
||||
# Недостача
|
||||
deficit_lines.append(line)
|
||||
elif line.difference > 0:
|
||||
# Излишек: создать новую партию
|
||||
InventoryProcessor._create_incoming_for_surplus(
|
||||
inventory, line
|
||||
)
|
||||
incomings_created += 1
|
||||
# Излишек
|
||||
surplus_lines.append(line)
|
||||
|
||||
# Отмечаем строку как обработанную
|
||||
line.processed = True
|
||||
@@ -78,6 +76,18 @@ class InventoryProcessor:
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
# Создаем WriteOffDocument для недостач (если есть)
|
||||
if deficit_lines:
|
||||
writeoff_document = InventoryProcessor._create_writeoff_document(
|
||||
inventory, deficit_lines
|
||||
)
|
||||
|
||||
# Создаем IncomingDocument для излишков (если есть)
|
||||
if surplus_lines:
|
||||
incoming_document = InventoryProcessor._create_incoming_document(
|
||||
inventory, surplus_lines
|
||||
)
|
||||
|
||||
# Обновляем статус инвентаризации
|
||||
inventory.status = 'completed'
|
||||
inventory.save(update_fields=['status'])
|
||||
@@ -91,78 +101,101 @@ class InventoryProcessor:
|
||||
return {
|
||||
'inventory': inventory,
|
||||
'processed_lines': lines.count(),
|
||||
'writeoffs_created': writeoffs_created,
|
||||
'incomings_created': incomings_created,
|
||||
'writeoff_document': writeoff_document,
|
||||
'incoming_document': incoming_document,
|
||||
'errors': errors
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _create_writeoff_for_deficit(inventory, line):
|
||||
def _create_writeoff_document(inventory, deficit_lines):
|
||||
"""
|
||||
Создать операцию WriteOff для недостачи при инвентаризации.
|
||||
Списывается по FIFO из старейших партий.
|
||||
Создать документ списания (WriteOffDocument) для недостач при инвентаризации.
|
||||
Документ создается в статусе 'draft' и не проводится сразу.
|
||||
|
||||
Args:
|
||||
inventory: объект Inventory
|
||||
line: объект InventoryLine с negative difference
|
||||
"""
|
||||
quantity_to_writeoff = abs(line.difference)
|
||||
deficit_lines: список InventoryLine с negative difference
|
||||
|
||||
# Списываем по FIFO
|
||||
allocations = StockBatchManager.write_off_by_fifo(
|
||||
line.product,
|
||||
inventory.warehouse,
|
||||
quantity_to_writeoff
|
||||
Returns:
|
||||
WriteOffDocument
|
||||
"""
|
||||
if not deficit_lines:
|
||||
return None
|
||||
|
||||
# Создаем документ списания (черновик)
|
||||
writeoff_document = WriteOffDocumentService.create_document(
|
||||
warehouse=inventory.warehouse,
|
||||
date=inventory.date.date(),
|
||||
notes=f'Списание по результатам инвентаризации #{inventory.id}',
|
||||
created_by=None # Можно добавить пользователя если передавать в process_inventory
|
||||
)
|
||||
|
||||
# Создаем WriteOff для каждой партии
|
||||
for batch, qty_allocated in allocations:
|
||||
WriteOff.objects.create(
|
||||
batch=batch,
|
||||
quantity=qty_allocated,
|
||||
# Связываем документ с инвентаризацией
|
||||
writeoff_document.inventory = inventory
|
||||
writeoff_document.save(update_fields=['inventory'])
|
||||
|
||||
# Добавляем позиции в документ
|
||||
for line in deficit_lines:
|
||||
quantity_to_writeoff = abs(line.difference)
|
||||
WriteOffDocumentService.add_item(
|
||||
document=writeoff_document,
|
||||
product=line.product,
|
||||
quantity=quantity_to_writeoff,
|
||||
reason='inventory',
|
||||
cost_price=batch.cost_price,
|
||||
notes=f'Инвентаризация {inventory.id}, строка {line.id}'
|
||||
notes=f'Инвентаризация #{inventory.id}, строка #{line.id}'
|
||||
)
|
||||
|
||||
return writeoff_document
|
||||
|
||||
@staticmethod
|
||||
def _create_incoming_for_surplus(inventory, line):
|
||||
def _create_incoming_document(inventory, surplus_lines):
|
||||
"""
|
||||
Создать операцию Incoming для излишка при инвентаризации.
|
||||
Новая партия создается с последней известной cost_price товара.
|
||||
Создать документ поступления (IncomingDocument) для излишков при инвентаризации.
|
||||
Документ создается в статусе 'draft' и не проводится сразу.
|
||||
|
||||
Args:
|
||||
inventory: объект Inventory
|
||||
line: объект InventoryLine с positive difference
|
||||
surplus_lines: список InventoryLine с positive difference
|
||||
|
||||
Returns:
|
||||
IncomingDocument
|
||||
"""
|
||||
quantity_surplus = line.difference
|
||||
if not surplus_lines:
|
||||
return None
|
||||
|
||||
# Получаем последнюю known cost_price
|
||||
cost_price = InventoryProcessor._get_last_cost_price(
|
||||
line.product,
|
||||
inventory.warehouse
|
||||
)
|
||||
|
||||
# Генерируем номер документа для поступления
|
||||
document_number = generate_incoming_document_number()
|
||||
|
||||
# Создаем IncomingBatch с типом 'inventory'
|
||||
incoming_batch = IncomingBatch.objects.create(
|
||||
# Создаем документ поступления (черновик) с типом 'inventory'
|
||||
incoming_document = IncomingDocumentService.create_document(
|
||||
warehouse=inventory.warehouse,
|
||||
document_number=document_number,
|
||||
date=inventory.date.date(),
|
||||
receipt_type='inventory',
|
||||
notes=f'Оприходование при инвентаризации {inventory.id}, строка {line.id}'
|
||||
supplier_name=None,
|
||||
notes=f'Оприходование по результатам инвентаризации #{inventory.id}',
|
||||
created_by=None # Можно добавить пользователя если передавать в process_inventory
|
||||
)
|
||||
|
||||
# Создаем документ Incoming
|
||||
# Сигнал create_stock_batch_on_incoming автоматически создаст StockBatch
|
||||
Incoming.objects.create(
|
||||
batch=incoming_batch,
|
||||
product=line.product,
|
||||
quantity=quantity_surplus,
|
||||
cost_price=cost_price,
|
||||
notes=f'Инвентаризация {inventory.id}, строка {line.id}'
|
||||
)
|
||||
# Связываем документ с инвентаризацией
|
||||
incoming_document.inventory = inventory
|
||||
incoming_document.save(update_fields=['inventory'])
|
||||
|
||||
# Добавляем позиции в документ
|
||||
for line in surplus_lines:
|
||||
quantity_surplus = line.difference
|
||||
|
||||
# Получаем последнюю known cost_price
|
||||
cost_price = InventoryProcessor._get_last_cost_price(
|
||||
line.product,
|
||||
inventory.warehouse
|
||||
)
|
||||
|
||||
IncomingDocumentService.add_item(
|
||||
document=incoming_document,
|
||||
product=line.product,
|
||||
quantity=quantity_surplus,
|
||||
cost_price=cost_price,
|
||||
notes=f'Инвентаризация #{inventory.id}, строка #{line.id}'
|
||||
)
|
||||
|
||||
return incoming_document
|
||||
|
||||
@staticmethod
|
||||
def _get_last_cost_price(product, warehouse):
|
||||
|
||||
Reference in New Issue
Block a user