Show procurement arrival notice timestamp
This commit is contained in:
parent
b16b74c060
commit
947883243c
@ -131,6 +131,10 @@ def _effective_received_quantity(item):
|
||||
|
||||
class ProcurementRequestSummaryMixin(serializers.Serializer):
|
||||
is_viewed = serializers.SerializerMethodField()
|
||||
last_arrival_notice_at = serializers.DateTimeField(
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
next_expected_delivery_date = serializers.SerializerMethodField()
|
||||
items_total_count = serializers.SerializerMethodField()
|
||||
items_received_count = serializers.SerializerMethodField()
|
||||
@ -545,6 +549,7 @@ class ProcurementRequestListSerializer(
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"is_viewed",
|
||||
"last_arrival_notice_at",
|
||||
"total_cost",
|
||||
"items_count",
|
||||
"next_expected_delivery_date",
|
||||
@ -582,6 +587,7 @@ class ProcurementRequestListSerializer(
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"is_viewed",
|
||||
"last_arrival_notice_at",
|
||||
"items_count",
|
||||
"total_cost",
|
||||
"next_expected_delivery_date",
|
||||
@ -785,6 +791,7 @@ class ProcurementRequestDetailSerializer(
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"is_viewed",
|
||||
"last_arrival_notice_at",
|
||||
"total_cost",
|
||||
"actual_cost",
|
||||
"next_expected_delivery_date",
|
||||
@ -820,6 +827,7 @@ class ProcurementRequestDetailSerializer(
|
||||
"completed_at",
|
||||
"is_editable",
|
||||
"is_viewed",
|
||||
"last_arrival_notice_at",
|
||||
"total_cost",
|
||||
"next_expected_delivery_date",
|
||||
"items_total_count",
|
||||
|
||||
@ -35,6 +35,7 @@ from procurement.notifications.handlers import (
|
||||
notify_request_arrival,
|
||||
notify_request_comment,
|
||||
)
|
||||
from procurement.notifications.config import NotificationVerbs
|
||||
from communications import comments_helpers
|
||||
from communications.models import Message
|
||||
from procurement.services import ProcurementApprovalResolver
|
||||
@ -100,6 +101,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
"complete",
|
||||
"cancel",
|
||||
"mark_all_received",
|
||||
"notify_arrival",
|
||||
"set_viewed",
|
||||
]:
|
||||
return ProcurementRequestDetailSerializer
|
||||
@ -122,16 +124,19 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
Count,
|
||||
DateTimeField,
|
||||
Exists,
|
||||
IntegerField,
|
||||
OuterRef,
|
||||
Subquery,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Cast, Coalesce
|
||||
from django.utils import timezone
|
||||
from communications.models import Chat
|
||||
from notifications.models import Notification
|
||||
|
||||
ct = ContentType.objects.get_for_model(ProcurementRequest)
|
||||
comments_sub = (
|
||||
@ -147,6 +152,18 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
.values("msg_count")[:1]
|
||||
)
|
||||
arrival_notice_sub = (
|
||||
Notification.objects.filter(
|
||||
verb=NotificationVerbs.ARRIVAL_NOTICE,
|
||||
target_content_type=ct,
|
||||
target_object_id=Cast(
|
||||
OuterRef("pk"),
|
||||
output_field=CharField(),
|
||||
),
|
||||
)
|
||||
.order_by("-timestamp")
|
||||
.values("timestamp")[:1]
|
||||
)
|
||||
|
||||
user = self.request.user
|
||||
viewed_annotation = (
|
||||
@ -168,6 +185,10 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
output_field=IntegerField(),
|
||||
),
|
||||
is_viewed_for_current_user=viewed_annotation,
|
||||
last_arrival_notice_at=Subquery(
|
||||
arrival_notice_sub,
|
||||
output_field=DateTimeField(),
|
||||
),
|
||||
)
|
||||
scope = self.request.query_params.get("scope", None)
|
||||
period = self.request.query_params.get("period", None)
|
||||
@ -919,6 +940,24 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
notify_request_arrival(procurement_request, actor=request.user)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from notifications.models import Notification
|
||||
|
||||
request_content_type = ContentType.objects.get_for_model(
|
||||
ProcurementRequest,
|
||||
)
|
||||
latest_notice_at = (
|
||||
Notification.objects.filter(
|
||||
verb=NotificationVerbs.ARRIVAL_NOTICE,
|
||||
target_content_type=request_content_type,
|
||||
target_object_id=str(procurement_request.pk),
|
||||
)
|
||||
.order_by("-timestamp")
|
||||
.values_list("timestamp", flat=True)
|
||||
.first()
|
||||
)
|
||||
if latest_notice_at:
|
||||
procurement_request.last_arrival_notice_at = latest_notice_at
|
||||
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -8,6 +8,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils import timezone
|
||||
from push_notifications.models import WebPushDevice
|
||||
from rest_framework import status
|
||||
@ -3117,6 +3118,59 @@ class TestProcessingDepartmentWorkflow:
|
||||
for item in arrival_notifications
|
||||
)
|
||||
|
||||
def test_notify_arrival_exposes_last_notice_timestamp(
|
||||
self, api_client, user, supply_user, processing_request
|
||||
):
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-notify-arrival',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
notice = Notification.objects.get(
|
||||
recipient=user,
|
||||
verb="procurement_arrival_notice",
|
||||
target_object_id=str(processing_request.id),
|
||||
)
|
||||
assert (
|
||||
parse_datetime(response.data["last_arrival_notice_at"])
|
||||
== notice.timestamp
|
||||
)
|
||||
|
||||
list_response = api_client.get(
|
||||
reverse('api:v1:procurement:procurementrequest-list'),
|
||||
)
|
||||
assert list_response.status_code == status.HTTP_200_OK
|
||||
list_payload = list_response.json()
|
||||
list_items = list_payload.get("results", list_payload)
|
||||
list_item = next(
|
||||
item for item in list_items
|
||||
if item["id"] == processing_request.id
|
||||
)
|
||||
assert (
|
||||
parse_datetime(list_item["last_arrival_notice_at"])
|
||||
== notice.timestamp
|
||||
)
|
||||
|
||||
detail_response = api_client.get(
|
||||
reverse(
|
||||
'api:v1:procurement:procurementrequest-detail',
|
||||
kwargs={'pk': processing_request.id},
|
||||
),
|
||||
)
|
||||
assert detail_response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
parse_datetime(detail_response.data["last_arrival_notice_at"])
|
||||
== notice.timestamp
|
||||
)
|
||||
|
||||
def test_notify_arrival_is_not_available_after_request_completed(
|
||||
self, api_client, supply_user, processing_request, monkeypatch
|
||||
):
|
||||
|
||||
@ -582,6 +582,12 @@ html[data-bs-theme="dark"] .app-input::-webkit-calendar-picker-indicator {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.app-feedback-notify {
|
||||
border: 1px solid color-mix(in srgb, #8b5cf6 24%, var(--border-subtle));
|
||||
background: color-mix(in srgb, #8b5cf6 14%, var(--surface-primary));
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.attendance-matrix-cell--technical {
|
||||
background: color-mix(in srgb, #ef4444 14%, var(--surface-primary));
|
||||
color: #b91c1c;
|
||||
|
||||
@ -37,7 +37,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { CommentComposer, CommentDeleteButton } from "@/components/shared/CommentControls";
|
||||
import { SearchableSelectSingle } from "@/components/shared/SearchableSelect";
|
||||
import { formatDate, formatMoney } from "@/lib/shared";
|
||||
import { formatDate, formatDateTime, formatMoney } from "@/lib/shared";
|
||||
import { useProcurementPage } from "@/hooks/useProcurementPage";
|
||||
import { Modal } from "@/components/ui";
|
||||
import { getProcurementQuantityUnitLabel, getProcurementUnitOptions } from "@/lib/procurementUnits";
|
||||
@ -117,6 +117,7 @@ const periodOptions = [
|
||||
];
|
||||
|
||||
const fmt = formatDate;
|
||||
const fmtDateTime = formatDateTime;
|
||||
const money = formatMoney;
|
||||
type RequestActionDialogKind = "approve" | "reject" | "cancel" | "delete";
|
||||
|
||||
@ -859,6 +860,7 @@ export default function ProcurementPage() {
|
||||
resolvedDetail.quantity_unit_label ?? req.quantity_unit_label,
|
||||
);
|
||||
const nextExpectedDeliveryDate = resolvedDetail.next_expected_delivery_date ?? req.next_expected_delivery_date ?? null;
|
||||
const lastArrivalNoticeAt = resolvedDetail.last_arrival_notice_at ?? req.last_arrival_notice_at ?? null;
|
||||
const approvalsCount = resolvedDetail.approvals?.length ?? 0;
|
||||
const requestDateLabel = getRequestListDateLabel(resolvedDetail, st);
|
||||
|
||||
@ -1020,7 +1022,7 @@ export default function ProcurementPage() {
|
||||
<span>не назначен</span>
|
||||
)}
|
||||
</div>
|
||||
{(fulfillmentStatusDisplay || itemsTotalCount > 0 || approvalsCount > 0 || nextExpectedDeliveryDate) && (
|
||||
{(fulfillmentStatusDisplay || itemsTotalCount > 0 || approvalsCount > 0 || nextExpectedDeliveryDate || lastArrivalNoticeAt) && (
|
||||
<div className="col-span-2 flex flex-wrap items-center gap-2 pt-0.5">
|
||||
{fulfillmentStatusDisplay ? (
|
||||
<span className={`${getFulfillmentStatusClass(fulfillmentStatus)} inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium`}>
|
||||
@ -1032,6 +1034,11 @@ export default function ProcurementPage() {
|
||||
<CalendarDays size={11} /> {fmt(nextExpectedDeliveryDate)}
|
||||
</span>
|
||||
) : null}
|
||||
{lastArrivalNoticeAt ? (
|
||||
<span className="app-feedback-notify inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium">
|
||||
Уведомлён {fmtDateTime(lastArrivalNoticeAt)}
|
||||
</span>
|
||||
) : null}
|
||||
{itemsTotalCount > 0 && (
|
||||
<span className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium">
|
||||
<Package size={11} /> {itemsTotalCount} поз.
|
||||
|
||||
@ -595,6 +595,7 @@ export interface ProcurementRequest {
|
||||
total_cost?: string | number;
|
||||
total_estimated_cost?: string | number;
|
||||
items_count?: number;
|
||||
last_arrival_notice_at?: string | null;
|
||||
next_expected_delivery_date?: string | null;
|
||||
items_total_count?: number;
|
||||
items_received_count?: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user