120 lines
3.6 KiB
Python
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
|