"""Application use cases for creating and editing procurement requests.""" from collections.abc import Mapping, Sequence from typing import Any from django.core.exceptions import ValidationError from django.db import transaction from django.utils import timezone from communications import comments_helpers from procurement.constants import ApprovalStatus, ProcurementStatus from procurement.models import ( Approval, ProcurementItem, ProcurementRequest, ProcurementSettings, ) class ProcurementRequestUpdateError(Exception): """Expected conflict while applying a procurement request update.""" class ProcurementRequestItemNotFound(ProcurementRequestUpdateError): """An update referenced an item that does not belong to the request.""" def _create_initial_comment(*, item, author, content: str) -> None: if content and author and author.is_authenticated: comments_helpers.create_comment( obj=item, author=author, content=content, ) @transaction.atomic def create_procurement_request( *, requestor, attributes: Mapping[str, Any], items: Sequence[Mapping[str, Any]], ) -> ProcurementRequest: """Create a request and all initial items as one atomic operation.""" request_attributes = dict(attributes) if request_attributes.get("processing_department"): request_attributes["status"] = ProcurementStatus.WAITING procurement_request = ProcurementRequest( requestor=requestor, **request_attributes, ) procurement_request._notification_actor = requestor procurement_request.save() for raw_item in items: item_attributes = dict(raw_item) initial_comment = item_attributes.pop("initial_comment", "").strip() item = ProcurementItem.objects.create( request=procurement_request, **item_attributes, ) _create_initial_comment( item=item, author=requestor, content=initial_comment, ) if procurement_request.request_type == ProcurementRequest.RequestType.SUBSCRIPTION: approver_id = ProcurementSettings.get_solo().subscription_approver_id if not approver_id: raise ValidationError( "В настройках закупок не выбран согласующий заявок на подписки." ) Approval.objects.create( request=procurement_request, approver_id=approver_id, priority=0, step_name="Согласование подписки", status=ApprovalStatus.PENDING, ) procurement_request.status = ProcurementStatus.PENDING procurement_request.submitted_at = timezone.now() procurement_request.save( update_fields=["status", "submitted_at", "updated_at"], ) return procurement_request @transaction.atomic def update_procurement_request_with_items( *, request_id: int, attributes: Mapping[str, Any], items: Sequence[Mapping[str, Any]], author, ) -> ProcurementRequest: """Update request fields and synchronize its item collection atomically.""" procurement_request = ProcurementRequest.objects.select_for_update().get( pk=request_id, ) projects = attributes.pop("projects", None) existing_items = { item.id: item for item in procurement_request.items.select_for_update().all() } for field, value in attributes.items(): setattr(procurement_request, field, value) procurement_request.save() if projects is not None: procurement_request.projects.set(projects) kept_item_ids: set[int] = set() for raw_item in items: item_attributes = dict(raw_item) item_id = item_attributes.pop("id", None) initial_comment = item_attributes.pop("initial_comment", "").strip() if item_id is None: item = ProcurementItem.objects.create( request=procurement_request, **item_attributes, ) else: item = existing_items.get(item_id) if item is None: raise ProcurementRequestItemNotFound(item_id) for field, value in item_attributes.items(): setattr(item, field, value) item.save() kept_item_ids.add(item.id) _create_initial_comment( item=item, author=author, content=initial_comment, ) obsolete_item_ids = set(existing_items) - kept_item_ids if obsolete_item_ids: ProcurementItem.objects.filter( request=procurement_request, id__in=obsolete_item_ids, ).delete() procurement_request.recalculate_fulfillment_status(save=True) return procurement_request