Files
octopus/myproject/inventory/services/inventory_processor.py
Andrey Smakotin 6735be9b08 feat: Реализовать систему поступления товаров с партиями (IncomingBatch)
Основные изменения:
- Создана модель IncomingBatch для группировки товаров по документам
- Каждое поступление (Incoming) связано с одной батчем поступления
- Автоматическое создание StockBatch для каждого товара в приходе
- Реализована система нумерации партий (IN-XXXX-XXXX) с поиском максимума в БД
- Обновлены все представления (views) для работы с новой архитектурой
- Добавлены детальные страницы просмотра партий поступлений
- Обновлены шаблоны для отображения информации о партиях и их товарах
- Исправлена логика сигналов для создания StockBatch при приходе товара
- Обновлены формы для работы с новой структурой IncomingBatch

Архитектура FIFO:
- IncomingBatch: одна партия поступления (номер IN-XXXX-XXXX)
- Incoming: товар в партии поступления
- StockBatch: одна партия товара на складе (создается для каждого товара)

Это позволяет системе правильно применять FIFO при продаже товаров.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 03:26:06 +03:00

287 lines
10 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,
StockBatch, Stock
)
from inventory.services.batch_manager import StockBatchManager
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
)
# Создаем новую партию
batch = StockBatchManager.create_batch(
line.product,
inventory.warehouse,
quantity_surplus,
cost_price
)
# Создаем документ Incoming
Incoming.objects.create(
product=line.product,
warehouse=inventory.warehouse,
quantity=quantity_surplus,
cost_price=cost_price,
batch=batch,
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
}