121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
"""Shared validation and aggregation helpers for procurement serializers."""
|
||
|
||
import re
|
||
from datetime import date
|
||
|
||
from rest_framework import serializers
|
||
|
||
from procurement.constants import ProcurementItemExecutionStatus
|
||
from procurement.models import ProcurementRequest, ProcurementSettings
|
||
|
||
PROBLEM_ITEM_STATUSES = {
|
||
ProcurementItemExecutionStatus.REJECTED,
|
||
ProcurementItemExecutionStatus.COMPLETED_WITH_ISSUE,
|
||
ProcurementItemExecutionStatus.EDITED,
|
||
ProcurementItemExecutionStatus.DEFECTIVE,
|
||
}
|
||
|
||
|
||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||
UNIT_DISPLAY_LABELS = {
|
||
"шт": "штуки",
|
||
"компл": "комплекты",
|
||
"упак": "упаковки",
|
||
"короб": "короба",
|
||
"пачка": "пачки",
|
||
"кг": "килограммы",
|
||
"г": "граммы",
|
||
"л": "литры",
|
||
"мл": "миллилитры",
|
||
"м": "метры",
|
||
"кв. м": "кв. метры",
|
||
"куб. м": "куб. метры",
|
||
"рулон": "рулоны",
|
||
"пара": "пары",
|
||
"услуга": "услуги",
|
||
"час": "часы",
|
||
}
|
||
|
||
|
||
def _normalize_unit(value):
|
||
return re.sub(r"\s+", " ", str(value or "").strip()).lower()
|
||
|
||
|
||
def _unit_display_label(value):
|
||
unit = _normalize_unit(value)
|
||
if not unit:
|
||
return "единиц"
|
||
return UNIT_DISPLAY_LABELS.get(unit, unit)
|
||
|
||
|
||
def _validate_expected_delivery_dates(value):
|
||
if value in (None, ""):
|
||
return []
|
||
if not isinstance(value, list):
|
||
raise serializers.ValidationError("Ожидаемые даты должны быть списком.")
|
||
|
||
result = []
|
||
for raw in value:
|
||
item = str(raw or "").strip()
|
||
if not item:
|
||
continue
|
||
if not DATE_RE.fullmatch(item):
|
||
raise serializers.ValidationError(
|
||
"Каждая ожидаемая дата должна быть в формате YYYY-MM-DD."
|
||
)
|
||
try:
|
||
parsed = date.fromisoformat(item)
|
||
except ValueError as exc:
|
||
raise serializers.ValidationError(
|
||
"Каждая ожидаемая дата должна быть корректной календарной датой."
|
||
) from exc
|
||
result.append(parsed.isoformat())
|
||
return result
|
||
|
||
|
||
def _expected_delivery_date_values(item):
|
||
dates = []
|
||
for raw in item.expected_delivery_dates or []:
|
||
if not isinstance(raw, str) or not DATE_RE.fullmatch(raw):
|
||
continue
|
||
try:
|
||
dates.append(date.fromisoformat(raw))
|
||
except ValueError:
|
||
continue
|
||
return dates
|
||
|
||
|
||
def _validate_processing_department_available(
|
||
processing_department,
|
||
request_type=ProcurementRequest.RequestType.STANDARD,
|
||
):
|
||
if not processing_department:
|
||
return
|
||
|
||
settings = ProcurementSettings.get_solo()
|
||
if request_type == ProcurementRequest.RequestType.SUBSCRIPTION:
|
||
available_departments = (
|
||
settings.subscription_available_processing_departments.all()
|
||
)
|
||
error_message = "Выберите отдел-исполнитель из допустимых для подписок."
|
||
else:
|
||
available_departments = settings.available_processing_departments.all()
|
||
error_message = "Выберите отдел-исполнитель из доступных для закупок."
|
||
if (
|
||
available_departments.exists()
|
||
and not available_departments.filter(pk=processing_department.pk).exists()
|
||
):
|
||
raise serializers.ValidationError({"processing_department": error_message})
|
||
|
||
|
||
def _request_items(obj):
|
||
return list(obj.items.all())
|
||
|
||
|
||
def _effective_ordered_quantity(item):
|
||
return item.effective_ordered_quantity
|
||
|
||
|
||
def _effective_received_quantity(item):
|
||
return item.effective_received_quantity
|