from collections.abc import Mapping from django.db import IntegrityError, transaction from django.utils import timezone from procurement.constants import EquipmentStatus, ProcurementStatus from procurement.models import Equipment, ProcurementItem class EquipmentCreationError(Exception): """Expected business-rule failure while creating equipment from an item.""" class ProcurementItemUnavailable(EquipmentCreationError): """The procurement item disappeared before the command acquired its lock.""" @transaction.atomic def create_equipment_from_procurement_item( *, item_id: int, attributes: Mapping[str, object], ) -> Equipment: """Create and link equipment as one concurrency-safe transaction.""" try: item = ( ProcurementItem.objects.select_for_update(of=("self", "request")) # Lock the item and its request, not the nullable equipment join. # equipment_id below is available without fetching Equipment. .select_related("request") .get(pk=item_id) ) except ProcurementItem.DoesNotExist as exc: raise ProcurementItemUnavailable("Позиция закупки не найдена.") from exc if item.request.status != ProcurementStatus.COMPLETED: raise EquipmentCreationError( "Оборудование можно создать только из завершённой заявки." ) if item.equipment_id is not None: raise EquipmentCreationError("Оборудование для этой позиции уже создано.") purchase_cost = item.actual_unit_price or item.estimated_unit_price if purchase_cost is None: raise EquipmentCreationError( "Для создания оборудования укажите фактическую или ориентировочную " "цену позиции." ) inventory_number = str(attributes["inventory_number"]) if Equipment.objects.filter(inventory_number=inventory_number).exists(): raise EquipmentCreationError( f'Инвентарный номер "{inventory_number}" уже используется.' ) try: equipment = Equipment.objects.create( name=item.name, inventory_number=inventory_number, serial_number=attributes.get("serial_number", ""), category=attributes["category"], department=attributes["department"], status=EquipmentStatus.AVAILABLE, responsible_person=attributes.get("responsible_person"), location=attributes.get("location", ""), purchase_date=timezone.localdate(), warranty_until=attributes.get("warranty_until"), purchase_cost=purchase_cost, notes=item.description, ) except IntegrityError as exc: raise EquipmentCreationError( "Не удалось создать оборудование: проверьте уникальность " "инвентарного номера." ) from exc item.equipment = equipment item.save(update_fields=["equipment"]) return equipment