procurement: use actual item price for approval totals

This commit is contained in:
lizerk 2026-05-26 17:43:28 +03:00
parent 6796f7e847
commit 5b422fd34e
3 changed files with 114 additions and 10 deletions

View File

@ -145,15 +145,30 @@ class ProcurementRequest(models.Model):
@property
def total_cost(self):
"""Общая стоимость заявки (сумма всех позиций)."""
"""Общая стоимость заявки по уточненным или ориентировочным ценам."""
effective_unit_price = Coalesce(
models.F("actual_unit_price"),
models.F("estimated_unit_price"),
Decimal("0.00"),
output_field=models.DecimalField(
max_digits=12,
decimal_places=2,
),
)
line_total = models.ExpressionWrapper(
effective_unit_price * models.F("quantity"),
output_field=models.DecimalField(
max_digits=18,
decimal_places=2,
),
)
total = self.items.aggregate(
total=Sum(
Coalesce(
models.F("estimated_unit_price"),
Decimal("0.00"),
output_field=models.DecimalField(),
) * models.F("quantity"),
output_field=models.DecimalField(),
line_total,
output_field=models.DecimalField(
max_digits=18,
decimal_places=2,
),
)
)["total"]
return total or Decimal("0.00")
@ -338,10 +353,13 @@ class ProcurementItem(models.Model):
@property
def total_price(self):
"""Общая стоимость позиции."""
if self.estimated_unit_price is None:
"""Общая стоимость позиции по фактической или ориентировочной цене."""
unit_price = self.actual_unit_price
if unit_price is None:
unit_price = self.estimated_unit_price
if unit_price is None:
return Decimal("0.00")
return self.estimated_unit_price * self.quantity
return unit_price * self.quantity
def clean(self):
super().clean()

View File

@ -2189,6 +2189,42 @@ class TestProcurementRequestWorkflow:
procurement_request.approvals.order_by('priority').values_list('priority', flat=True)
) == expected_priorities
def test_submit_request_applies_amount_thresholds_by_actual_price(
self, api_client, user, department
):
"""Если закупщик указал фактическую цену, пороги берутся по ней."""
procurement_request = ProcurementRequest.objects.create(
title="Уточненная пороговая заявка",
description="Проверка маршрутов",
department=department,
requestor=user,
status=ProcurementStatus.DRAFT,
urgency=UrgencyLevel.MEDIUM,
)
ProcurementItem.objects.create(
request=procurement_request,
name="Позиция с уточненной ценой",
quantity=1,
unit="шт",
estimated_unit_price=Decimal("100.00"),
actual_unit_price=Decimal("120000.00"),
)
api_client.force_authenticate(user=user)
url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': procurement_request.id}
)
response = api_client.post(url)
assert response.status_code == status.HTTP_200_OK
procurement_request.refresh_from_db()
assert procurement_request.total_cost == Decimal("120000.00")
assert list(
procurement_request.approvals.order_by('priority').values_list('priority', flat=True)
) == [HEAD_PRIORITY, FINANCE_PRIORITY, DIRECTOR_PRIORITY]
def test_submit_request_without_department_head_returns_explicit_error(
self, api_client, user, department
):

View File

@ -171,6 +171,39 @@ class TestProcurementRequest:
assert FINANCE_PRIORITY in approvals
assert DIRECTOR_PRIORITY in approvals
def test_total_cost_uses_actual_price_before_estimated(
self, department, user
):
"""Фактическая цена при расчете заявки приоритетнее ориентировочной."""
request = ProcurementRequest.objects.create(
title="Уточненная закупка",
department=department,
requestor=user,
)
ProcurementItem.objects.create(
request=request,
name="Позиция с фактической ценой",
quantity=2,
unit="шт",
estimated_unit_price=Decimal("100.00"),
actual_unit_price=Decimal("80.00"),
)
ProcurementItem.objects.create(
request=request,
name="Позиция без фактической цены",
quantity=3,
unit="шт",
estimated_unit_price=Decimal("50.00"),
)
ProcurementItem.objects.create(
request=request,
name="Позиция без цены",
quantity=4,
unit="шт",
)
assert request.total_cost == Decimal("310.00")
def test_is_editable_draft(self, department, user):
"""Тест: черновик можно редактировать."""
request = ProcurementRequest.objects.create(
@ -232,6 +265,23 @@ class TestProcurementItem:
assert item.total_price == Decimal("5005.00")
def test_total_price_prefers_actual_unit_price(self, department, user):
"""Фактическая цена приоритетнее ориентировочной в сумме позиции."""
request = ProcurementRequest.objects.create(
title="Заявка", department=department, requestor=user
)
item = ProcurementItem.objects.create(
request=request,
name="Кабель HDMI",
quantity=10,
unit="шт",
estimated_unit_price=Decimal("500.50"),
actual_unit_price=Decimal("450.00"),
)
assert item.total_price == Decimal("4500.00")
@pytest.mark.django_db
class TestBudget: