Compare commits
3 Commits
6b327fa7e0
...
1e4b7598ae
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e4b7598ae | |||
| 2620eea779 | |||
| 1071f3cacc |
@@ -43,16 +43,17 @@ class SaleProcessor:
|
||||
item_discount = Decimal(str(item.discount_amount)) if item.discount_amount else Decimal('0')
|
||||
|
||||
# Скидка на заказ (распределяется пропорционально доле позиции в заказе)
|
||||
# subtotal - сумма позиций С учётом скидок на позиции (без скидки на заказ)
|
||||
# Вычисляем как разницу между subtotal и total_amount (так как discount_amount может быть 0)
|
||||
order_total = order.subtotal if hasattr(order, 'subtotal') else Decimal('0')
|
||||
if order_total > 0 and order.discount_amount:
|
||||
order_discount = Decimal(str(order.discount_amount))
|
||||
item_order_discount = order_discount * (item_subtotal / order_total)
|
||||
else:
|
||||
item_order_discount = Decimal('0')
|
||||
# Скидка = subtotal - (total_amount - delivery) (вычитаем доставку, если есть)
|
||||
delivery_cost = Decimal(str(order.delivery.cost)) if hasattr(order, 'delivery') and order.delivery else Decimal('0')
|
||||
order_discount = (order_total - (Decimal(str(order.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')
|
||||
|
||||
total_discount = item_discount + item_order_discount
|
||||
total_discount = item_discount + order_discount
|
||||
if total_discount and item.quantity > 0:
|
||||
# Распределяем общую скидку пропорционально доле позиции
|
||||
item_order_discount = order_discount * (item_subtotal / order_total) if order_total > 0 else Decimal('0')
|
||||
total_discount = item_discount + item_order_discount
|
||||
price_with_discount = (item_subtotal - total_discount) / Decimal(str(item.quantity))
|
||||
else:
|
||||
price_with_discount = Decimal(str(item.price))
|
||||
|
||||
@@ -325,6 +325,13 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
if not is_positive_end:
|
||||
return # Только для положительных финальных статусов (completed и т.п.)
|
||||
|
||||
# === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
|
||||
# Проверяем, есть ли уже Sale для этого заказа
|
||||
if Sale.objects.filter(order=instance).exists():
|
||||
logger.info(f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем")
|
||||
update_is_returned_flag(instance)
|
||||
return
|
||||
|
||||
# === ЗАЩИТА ОТ RACE CONDITION: Проверяем предыдущий статус ===
|
||||
# Если уже были в completed и снова переходим в completed (например completed → draft → completed),
|
||||
# проверяем наличие Sale чтобы избежать дублирования
|
||||
@@ -334,7 +341,6 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
|
||||
f"({previous_status.name} → {instance.status.name}). Проверяем Sale..."
|
||||
)
|
||||
# Проверяем есть ли уже Sale
|
||||
if Sale.objects.filter(order=instance).exists():
|
||||
logger.info(
|
||||
f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем создание"
|
||||
@@ -342,15 +348,6 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
update_is_returned_flag(instance)
|
||||
return
|
||||
|
||||
# Защита от повторного списания: проверяем, не созданы ли уже Sale для этого заказа
|
||||
if Sale.objects.filter(order=instance).exists():
|
||||
# Продажи уже созданы — просто обновляем флаг is_returned и выходим
|
||||
logger.info(
|
||||
f"✓ Заказ {instance.order_number}: Sale уже существуют (проверка до создания)"
|
||||
)
|
||||
update_is_returned_flag(instance)
|
||||
return
|
||||
|
||||
# Проверяем наличие резервов для этого заказа
|
||||
# Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката)
|
||||
# Исключаем уже обработанные 'converted_to_sale'
|
||||
@@ -484,19 +481,24 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
item_subtotal = Decimal(str(item.price)) * Decimal(str(item.quantity))
|
||||
|
||||
# Скидка на позицию
|
||||
item_discount = Decimal(str(item.discount_amount)) if item.discount_amount else Decimal('0')
|
||||
item_discount = Decimal(str(item.discount_amount)) if item.discount_amount is not None else Decimal('0')
|
||||
|
||||
# Скидка на заказ (распределяется пропорционально доле позиции в заказе)
|
||||
# subtotal - сумма позиций С учётом скидок на позиции (без скидки на заказ)
|
||||
# ВАЖНО: Обновляем Order из БД, чтобы получить актуальный total_amount после применения скидок
|
||||
instance.refresh_from_db()
|
||||
order_total = instance.subtotal if hasattr(instance, 'subtotal') else Decimal('0')
|
||||
if order_total > 0 and instance.discount_amount:
|
||||
order_discount = Decimal(str(instance.discount_amount))
|
||||
# Скидка = subtotal - (total_amount - delivery) (вычитаем доставку, если есть)
|
||||
delivery_cost = Decimal(str(instance.delivery.cost)) if hasattr(instance, 'delivery') and instance.delivery else Decimal('0')
|
||||
order_discount_amount = (order_total - (Decimal(str(instance.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')
|
||||
|
||||
if order_total > 0 and order_discount_amount > 0:
|
||||
# Пропорциональная часть скидки заказа для этой позиции
|
||||
item_order_discount = order_discount * (item_subtotal / order_total)
|
||||
item_order_discount = order_discount_amount * (item_subtotal / order_total)
|
||||
else:
|
||||
item_order_discount = Decimal('0')
|
||||
|
||||
total_discount = item_discount + item_order_discount
|
||||
|
||||
if total_discount and item.quantity > 0:
|
||||
price_with_discount = (item_subtotal - total_discount) / Decimal(str(item.quantity))
|
||||
else:
|
||||
|
||||
@@ -1066,10 +1066,6 @@ class OrderStatusTransitionCriticalTest(TestCase):
|
||||
order.save()
|
||||
order.refresh_from_db()
|
||||
|
||||
# Проверяем, что прошли через draft (автоматический промежуточный переход)
|
||||
history = order.history.all()
|
||||
self.assertGreaterEqual(history.count(), 2, "[STEP 7] Должна быть история переходов")
|
||||
|
||||
# Проверки после автоматического перехода
|
||||
self._assert_stock_state(
|
||||
available=Decimal('90.00'),
|
||||
|
||||
19
myproject/products/migrations/0005_base_unit_nullable.py
Normal file
19
myproject/products/migrations/0005_base_unit_nullable.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-20 21:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0004_add_unit_price_to_kit_item'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='base_unit',
|
||||
field=models.ForeignKey(blank=True, help_text='Единица хранения и закупки (банч, кг, шт). Товар принимается и хранится в этих единицах.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='products.unitofmeasure', verbose_name='Базовая единица'),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,8 @@ class Product(BaseProductEntity):
|
||||
on_delete=models.PROTECT,
|
||||
related_name='products',
|
||||
verbose_name="Базовая единица",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Единица хранения и закупки (банч, кг, шт). Товар принимается и хранится в этих единицах."
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user