diff --git a/backend/procurement/models.py b/backend/procurement/models.py index 1f3ec029..09cabf73 100644 --- a/backend/procurement/models.py +++ b/backend/procurement/models.py @@ -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() diff --git a/backend/tests/api/v1/procurement/test_procurement_requests.py b/backend/tests/api/v1/procurement/test_procurement_requests.py index fbe79a6b..8e7175f6 100644 --- a/backend/tests/api/v1/procurement/test_procurement_requests.py +++ b/backend/tests/api/v1/procurement/test_procurement_requests.py @@ -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 ): diff --git a/backend/tests/procurement/test_models.py b/backend/tests/procurement/test_models.py index 1fa4c6c4..7e064b3f 100644 --- a/backend/tests/procurement/test_models.py +++ b/backend/tests/procurement/test_models.py @@ -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: