feat(inventory): add support for selling in negative stock

Implement functionality to allow sales even when stock is insufficient, tracking pending quantities and resolving them when new stock arrives via incoming documents. This includes new fields in Sale model (is_pending_cost, pending_quantity), updates to batch manager for negative write-offs, and signal handlers for automatic processing.

- Add is_pending_cost and pending_quantity fields to Sale model
- Modify write_off_by_fifo to support allow_negative flag and return pending quantity
- Update incoming document service to allocate pending sales to new batches
- Enhance sale processor and signals to handle pending sales
- Remove outdated tests.py file
- Add migration for new Sale fields
This commit is contained in:
2026-01-04 12:27:10 +03:00
parent 123f330a26
commit a03f3df086
7 changed files with 223 additions and 570 deletions

View File

@@ -147,6 +147,20 @@ class Sale(models.Model):
help_text="Название единицы продажи на момент продажи"
)
# === ПОЛЯ ДЛЯ ПРОДАЖ "В МИНУС" ===
is_pending_cost = models.BooleanField(
default=False,
verbose_name="Ожидает себестоимости",
help_text="True если продажа создана без партий (продажа 'в минус')"
)
pending_quantity = models.DecimalField(
max_digits=10,
decimal_places=3,
default=Decimal('0'),
verbose_name="Ожидающее количество",
help_text="Количество, ожидающее привязки к партиям при приёмке"
)
class Meta:
verbose_name = "Продажа"
verbose_name_plural = "Продажи"
@@ -155,6 +169,7 @@ class Sale(models.Model):
models.Index(fields=['product', 'warehouse']),
models.Index(fields=['date']),
models.Index(fields=['order']),
models.Index(fields=['is_pending_cost']),
]
def __str__(self):
@@ -727,21 +742,33 @@ class Stock(models.Model):
def refresh_from_batches(self):
"""
Пересчитать остатки из StockBatch.
Можно вызвать для синхронизации после операций.
Учитывает "ожидающие" продажи (продажи "в минус").
quantity_available может быть отрицательным!
"""
# Сумма из активных партий
total_qty = StockBatch.objects.filter(
product=self.product,
warehouse=self.warehouse,
is_active=True
).aggregate(models.Sum('quantity'))['quantity__sum'] or Decimal('0')
# Учитываем ожидающие продажи (уменьшают доступное количество)
pending_sales = Sale.objects.filter(
product=self.product,
warehouse=self.warehouse,
is_pending_cost=True
).aggregate(models.Sum('pending_quantity'))['pending_quantity__sum'] or Decimal('0')
# quantity_available может быть отрицательным при продажах "в минус"
self.quantity_available = total_qty - pending_sales
# Резервы остаются как есть
total_reserved = Reservation.objects.filter(
product=self.product,
warehouse=self.warehouse,
status='reserved'
).aggregate(models.Sum('quantity_base'))['quantity_base__sum'] or Decimal('0')
self.quantity_available = total_qty
self.quantity_reserved = total_reserved
self.save()