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
|
@staticmethod
|
||||||
def get_batches_for_fifo(product, warehouse):
|
def get_batches_for_fifo(product, warehouse):
|
||||||
"""
|
"""
|
||||||
Получить все активные партии товара на складе,
|
Получить все активные партии товара на складе для FIFO списания.
|
||||||
отсортированные по created_at (старые первыми для FIFO).
|
Возвращает ВСЕ партии с quantity > 0, отсортированные по created_at.
|
||||||
|
ВАЖНО: Логика учета резервов реализована в write_off_by_fifo().
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
product: объект Product
|
product: объект Product
|
||||||
@@ -72,6 +73,7 @@ class StockBatchManager:
|
|||||||
def write_off_by_fifo(product, warehouse, quantity_to_write_off):
|
def write_off_by_fifo(product, warehouse, quantity_to_write_off):
|
||||||
"""
|
"""
|
||||||
Списать товар по FIFO (старые партии первыми).
|
Списать товар по FIFO (старые партии первыми).
|
||||||
|
ВАЖНО: Учитывает зарезервированное количество товара.
|
||||||
Возвращает список (batch, written_off_quantity) кортежей.
|
Возвращает список (batch, written_off_quantity) кортежей.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -83,20 +85,44 @@ class StockBatchManager:
|
|||||||
list: [(batch, qty_written), ...] - какие партии и сколько списано
|
list: [(batch, qty_written), ...] - какие партии и сколько списано
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: если недостаточно товара на складе
|
ValueError: если недостаточно свободного товара на складе
|
||||||
"""
|
"""
|
||||||
|
from inventory.models import Reservation
|
||||||
|
|
||||||
remaining = quantity_to_write_off
|
remaining = quantity_to_write_off
|
||||||
allocations = []
|
allocations = []
|
||||||
|
|
||||||
|
# Получаем общее количество зарезервированного товара (все резервы со статусом 'reserved')
|
||||||
|
total_reserved = Reservation.objects.filter(
|
||||||
|
product=product,
|
||||||
|
warehouse=warehouse,
|
||||||
|
status='reserved'
|
||||||
|
).aggregate(total=Sum('quantity'))['total'] or Decimal('0')
|
||||||
|
|
||||||
# Получаем партии по FIFO
|
# Получаем партии по FIFO
|
||||||
batches = StockBatchManager.get_batches_for_fifo(product, warehouse)
|
batches = StockBatchManager.get_batches_for_fifo(product, warehouse)
|
||||||
|
|
||||||
|
# Проходим партии, списывая только СВОБОДНОЕ количество
|
||||||
|
reserved_remaining = total_reserved # Сколько резерва еще не распределено по партиям
|
||||||
|
|
||||||
for batch in batches:
|
for batch in batches:
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
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
|
batch.quantity -= qty_from_this_batch
|
||||||
@@ -114,8 +140,9 @@ class StockBatchManager:
|
|||||||
|
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Недостаточно товара на складе. "
|
f"Недостаточно СВОБОДНОГО товара на складе. "
|
||||||
f"Требуется {quantity_to_write_off}, доступно {quantity_to_write_off - remaining}"
|
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)
|
return render(request, self.template_name, {'form': form}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(id=product_id, is_active=True)
|
product = Product.objects.get(id=product_id, status='active')
|
||||||
products.append((product, quantity))
|
products.append((product, quantity))
|
||||||
except Product.DoesNotExist:
|
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)
|
return render(request, self.template_name, {'form': form}, status=400)
|
||||||
|
|
||||||
# Начинаем транзакцию
|
# Начинаем транзакцию
|
||||||
|
|||||||
@@ -1312,12 +1312,17 @@ def pos_checkout(request):
|
|||||||
|
|
||||||
# Атомарная операция
|
# Атомарная операция
|
||||||
with db_transaction.atomic():
|
with db_transaction.atomic():
|
||||||
# 1. Создаём заказ
|
# 1. Создаём заказ с автоматическим проставлением текущей даты и времени
|
||||||
|
now = timezone.now()
|
||||||
|
current_time = now.time()
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
customer=customer,
|
customer=customer,
|
||||||
is_delivery=False, # POS - всегда самовывоз
|
is_delivery=False, # POS - всегда самовывоз
|
||||||
pickup_warehouse=warehouse,
|
pickup_warehouse=warehouse,
|
||||||
status=completed_status, # Сразу "Выполнен"
|
status=completed_status, # Сразу "Выполнен"
|
||||||
|
delivery_date=now.date(), # Текущая дата
|
||||||
|
delivery_time_start=current_time, # Текущее время
|
||||||
|
delivery_time_end=current_time, # То же время (точное время)
|
||||||
special_instructions=order_notes,
|
special_instructions=order_notes,
|
||||||
modified_by=request.user
|
modified_by=request.user
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user