Files
octopus/myproject/inventory/services/inventory_processor.py
Andrey Smakotin 78dc9e9801 Добавлено разделение типов поступлений на склад
- Добавлено поле receipt_type в модель IncomingBatch с типами: supplier, inventory, adjustment
- Исправлен баг в InventoryProcessor: теперь корректно создается IncomingBatch при инвентаризации
- Создан IncomingAdjustmentCreateView для оприходования без инвентаризации
- Обновлены формы, шаблоны и админка для поддержки разных типов поступлений
- Добавлена навигация и URL для оприходования
- Тип поступления отображается в списках приходов и партий
2025-12-20 23:47:13 +03:00

291 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Процессор для обработки инвентаризации.
Основной функционал:
- Обработка расхождений между фактом и системой
- Автоматическое создание WriteOff для недостач (по FIFO)
- Автоматическое создание Incoming для излишков
"""
from decimal import Decimal
from django.db import transaction
from django.utils import timezone
from inventory.models import (
Inventory, InventoryLine, WriteOff, Incoming, IncomingBatch,
StockBatch, Stock
)
from inventory.services.batch_manager import StockBatchManager
from inventory.utils import generate_incoming_document_number
class InventoryProcessor:
"""
Обработчик инвентаризации с автоматической коррекцией остатков.
"""
@staticmethod
@transaction.atomic
def process_inventory(inventory_id):
"""
Обработать инвентаризацию:
- Для недостач (разница < 0): создать WriteOff по FIFO
- Для излишков (разница > 0): создать Incoming с новой партией
- Обновить статус inventory и lines
Args:
inventory_id: ID объекта Inventory
Returns:
dict: {
'inventory': Inventory,
'processed_lines': int,
'writeoffs_created': int,
'incomings_created': int,
'errors': [...]
}
"""
inventory = Inventory.objects.get(id=inventory_id)
lines = InventoryLine.objects.filter(inventory=inventory, processed=False)
writeoffs_created = 0
incomings_created = 0
errors = []
try:
for line in lines:
try:
if line.difference < 0:
# Недостача: списать по FIFO
InventoryProcessor._create_writeoff_for_deficit(
inventory, line
)
writeoffs_created += 1
elif line.difference > 0:
# Излишек: создать новую партию
InventoryProcessor._create_incoming_for_surplus(
inventory, line
)
incomings_created += 1
# Отмечаем строку как обработанную
line.processed = True
line.save(update_fields=['processed'])
except Exception as e:
errors.append({
'line': line,
'error': str(e)
})
# Обновляем статус инвентаризации
inventory.status = 'completed'
inventory.save(update_fields=['status'])
except Exception as e:
errors.append({
'inventory': inventory,
'error': str(e)
})
return {
'inventory': inventory,
'processed_lines': lines.count(),
'writeoffs_created': writeoffs_created,
'incomings_created': incomings_created,
'errors': errors
}
@staticmethod
def _create_writeoff_for_deficit(inventory, line):
"""
Создать операцию WriteOff для недостачи при инвентаризации.
Списывается по FIFO из старейших партий.
Args:
inventory: объект Inventory
line: объект InventoryLine с negative difference
"""
quantity_to_writeoff = abs(line.difference)
# Списываем по FIFO
allocations = StockBatchManager.write_off_by_fifo(
line.product,
inventory.warehouse,
quantity_to_writeoff
)
# Создаем WriteOff для каждой партии
for batch, qty_allocated in allocations:
WriteOff.objects.create(
batch=batch,
quantity=qty_allocated,
reason='inventory',
cost_price=batch.cost_price,
notes=f'Инвентаризация {inventory.id}, строка {line.id}'
)
@staticmethod
def _create_incoming_for_surplus(inventory, line):
"""
Создать операцию Incoming для излишка при инвентаризации.
Новая партия создается с последней известной cost_price товара.
Args:
inventory: объект Inventory
line: объект InventoryLine с positive difference
"""
quantity_surplus = line.difference
# Получаем последнюю 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(
warehouse=inventory.warehouse,
document_number=document_number,
receipt_type='inventory',
notes=f'Оприходование при инвентаризации {inventory.id}, строка {line.id}'
)
# Создаем документ 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}'
)
@staticmethod
def _get_last_cost_price(product, warehouse):
"""
Получить последнюю известную закупочную цену товара на складе.
Используется для создания новой партии при излишке.
Порядок поиска:
1. Последняя активная партия на этом складе
2. Последняя активная партия на любом складе
3. cost_price из карточки Product (если есть)
4. Дефолт 0 (если ничего не найдено)
Args:
product: объект Product
warehouse: объект Warehouse
Returns:
Decimal - закупочная цена
"""
from inventory.models import StockBatch
# Вариант 1: последняя партия на этом складе
last_batch = StockBatch.objects.filter(
product=product,
warehouse=warehouse,
is_active=True
).order_by('-created_at').first()
if last_batch:
return last_batch.cost_price
# Вариант 2: последняя партия на любом складе
last_batch_any = StockBatch.objects.filter(
product=product,
is_active=True
).order_by('-created_at').first()
if last_batch_any:
return last_batch_any.cost_price
# Вариант 3: cost_price из карточки товара
if product.cost_price:
return product.cost_price
# Вариант 4: ноль (не должно быть)
return Decimal('0')
@staticmethod
def create_inventory_lines_from_current_stock(inventory):
"""
Автоматически создать InventoryLine для всех товаров на складе.
Используется для удобства: оператор может сразу начать вводить фактические
количества, имея под рукой системные остатки.
Args:
inventory: объект Inventory
"""
from inventory.models import StockBatch
# Получаем все товары, которые есть на этом складе
batches = StockBatch.objects.filter(
warehouse=inventory.warehouse,
is_active=True
).values('product').distinct()
for batch_dict in batches:
product = batch_dict['product']
# Рассчитываем системный остаток
quantity_system = StockBatchManager.get_total_stock(product, inventory.warehouse)
# Создаем строку инвентаризации (факт будет заполнен оператором)
InventoryLine.objects.get_or_create(
inventory=inventory,
product_id=product,
defaults={
'quantity_system': quantity_system,
'quantity_fact': 0, # Оператор должен заполнить
}
)
@staticmethod
def get_inventory_report(inventory):
"""
Получить отчет по инвентаризации.
Args:
inventory: объект Inventory
Returns:
dict: {
'inventory': Inventory,
'total_lines': int,
'total_deficit': Decimal,
'total_surplus': Decimal,
'lines': [...]
}
"""
lines = InventoryLine.objects.filter(inventory=inventory).select_related('product')
total_deficit = Decimal('0')
total_surplus = Decimal('0')
lines_data = []
for line in lines:
if line.difference < 0:
total_deficit += abs(line.difference)
elif line.difference > 0:
total_surplus += line.difference
lines_data.append({
'line': line,
'system_value': line.quantity_system * line.product.cost_price,
'fact_value': line.quantity_fact * line.product.cost_price,
'value_difference': (line.quantity_fact - line.quantity_system) * line.product.cost_price,
})
return {
'inventory': inventory,
'total_lines': lines.count(),
'total_deficit': total_deficit,
'total_surplus': total_surplus,
'lines': lines_data
}