All checks were successful
Deploy production / deploy (push) Successful in 13m7s
149 lines
4.8 KiB
Python
149 lines
4.8 KiB
Python
"""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
|