Fix: FIFO учитывает резервы, автоматическая дата/время в POS, исправлен фильтр Product в transfers

- Исправлен метод write_off_by_fifo() для учета зарезервированных партий
- Добавлено автоматическое проставление даты и времени при создании заказов в POS
- Исправлена ошибка фильтрации Product (is_active -> status='active') в transfers
- Предотвращает списание из зарезервированных партий, устраняя отрицательные остатки

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-05 23:40:03 +03:00
parent 456ae0b742
commit 2f7fed4a1a
3 changed files with 42 additions and 10 deletions

View File

@@ -22,8 +22,9 @@ class StockBatchManager:
@staticmethod
def get_batches_for_fifo(product, warehouse):
"""
Получить все активные партии товара на складе,
отсортированные по created_at (старые первыми для FIFO).
Получить все активные партии товара на складе для FIFO списания.
Возвращает ВСЕ партии с quantity > 0, отсортированные по created_at.
ВАЖНО: Логика учета резервов реализована в write_off_by_fifo().
Args:
product: объект Product
@@ -72,6 +73,7 @@ class StockBatchManager:
def write_off_by_fifo(product, warehouse, quantity_to_write_off):
"""
Списать товар по FIFO (старые партии первыми).
ВАЖНО: Учитывает зарезервированное количество товара.
Возвращает список (batch, written_off_quantity) кортежей.
Args:
@@ -83,20 +85,44 @@ class StockBatchManager:
list: [(batch, qty_written), ...] - какие партии и сколько списано
Raises:
ValueError: если недостаточно товара на складе
ValueError: если недостаточно свободного товара на складе
"""
from inventory.models import Reservation
remaining = quantity_to_write_off
allocations = []
# Получаем общее количество зарезервированного товара (все резервы со статусом 'reserved')
total_reserved = Reservation.objects.filter(
product=product,
warehouse=warehouse,
status='reserved'
).aggregate(total=Sum('quantity'))['total'] or Decimal('0')
# Получаем партии по FIFO
batches = StockBatchManager.get_batches_for_fifo(product, warehouse)
# Проходим партии, списывая только СВОБОДНОЕ количество
reserved_remaining = total_reserved # Сколько резерва еще не распределено по партиям
for batch in batches:
if remaining <= 0:
break
# Сколько можем списать из этой партии
qty_from_this_batch = min(batch.quantity, remaining)
# Определяем сколько в этой партии зарезервировано (пропорционально)
# Логика: старые партии "съедают" резерв первыми (как и при списании)
batch_reserved = min(batch.quantity, reserved_remaining)
reserved_remaining -= batch_reserved
# Свободное количество в партии
batch_free = batch.quantity - batch_reserved
if batch_free <= 0:
# Партия полностью зарезервирована → пропускаем
continue
# Сколько можем списать из этой партии (только свободное)
qty_from_this_batch = min(batch_free, remaining)
# Списываем
batch.quantity -= qty_from_this_batch
@@ -114,8 +140,9 @@ class StockBatchManager:
if remaining > 0:
raise ValueError(
f"Недостаточно товара на складе. "
f"Требуется {quantity_to_write_off}, доступно {quantity_to_write_off - remaining}"
f"Недостаточно СВОБОДНОГО товара на складе. "
f"Требуется {quantity_to_write_off}, доступно {quantity_to_write_off - remaining}. "
f"(Общий резерв: {total_reserved})"
)
# Обновляем кеш остатков

View File

@@ -91,10 +91,10 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
return render(request, self.template_name, {'form': form}, status=400)
try:
product = Product.objects.get(id=product_id, is_active=True)
product = Product.objects.get(id=product_id, status='active')
products.append((product, quantity))
except Product.DoesNotExist:
messages.error(request, f'Товар с ID {product_id} не найден')
messages.error(request, f'Товар с ID {product_id} не найден или неактивен')
return render(request, self.template_name, {'form': form}, status=400)
# Начинаем транзакцию