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:
@@ -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})"
|
||||
)
|
||||
|
||||
# Обновляем кеш остатков
|
||||
|
||||
@@ -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)
|
||||
|
||||
# Начинаем транзакцию
|
||||
|
||||
Reference in New Issue
Block a user