From 2f7fed4a1aa05f1c9d14b50b46aa8ad4e3998d1e Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Fri, 5 Dec 2025 23:40:03 +0300 Subject: [PATCH] =?UTF-8?q?Fix:=20FIFO=20=D1=83=D1=87=D0=B8=D1=82=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D1=8B,=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B0=D1=8F=20=D0=B4=D0=B0=D1=82=D0=B0?= =?UTF-8?q?/=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=B2=20POS,=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D1=80=20Product=20=D0=B2=20transfers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлен метод write_off_by_fifo() для учета зарезервированных партий - Добавлено автоматическое проставление даты и времени при создании заказов в POS - Исправлена ошибка фильтрации Product (is_active -> status='active') в transfers - Предотвращает списание из зарезервированных партий, устраняя отрицательные остатки 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- myproject/inventory/services/batch_manager.py | 41 +++++++++++++++---- myproject/inventory/views/transfer.py | 4 +- myproject/pos/views.py | 7 +++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/myproject/inventory/services/batch_manager.py b/myproject/inventory/services/batch_manager.py index f624ba8..f64222e 100644 --- a/myproject/inventory/services/batch_manager.py +++ b/myproject/inventory/services/batch_manager.py @@ -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})" ) # Обновляем кеш остатков diff --git a/myproject/inventory/views/transfer.py b/myproject/inventory/views/transfer.py index 5ea4c16..7c885ce 100644 --- a/myproject/inventory/views/transfer.py +++ b/myproject/inventory/views/transfer.py @@ -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) # Начинаем транзакцию diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 8931f12..dfe4536 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -1312,12 +1312,17 @@ def pos_checkout(request): # Атомарная операция with db_transaction.atomic(): - # 1. Создаём заказ + # 1. Создаём заказ с автоматическим проставлением текущей даты и времени + now = timezone.now() + current_time = now.time() order = Order.objects.create( customer=customer, is_delivery=False, # POS - всегда самовывоз pickup_warehouse=warehouse, status=completed_status, # Сразу "Выполнен" + delivery_date=now.date(), # Текущая дата + delivery_time_start=current_time, # Текущее время + delivery_time_end=current_time, # То же время (точное время) special_instructions=order_notes, modified_by=request.user )