120 lines
3.6 KiB
Python

"""Application use cases for creating and editing procurement requests."""
from collections.abc import Mapping, Sequence
from typing import Any
from django.db import transaction
from communications import comments_helpers
from procurement.constants import ProcurementStatus
from procurement.models import ProcurementItem, ProcurementRequest
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,
)
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,
)
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()
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