procurement: use actual item price for approval totals
This commit is contained in:
parent
6796f7e847
commit
5b422fd34e
@ -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()
|
||||
|
||||
@ -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
|
||||
):
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user