Support multiple expected delivery dates
This commit is contained in:
parent
0338580bfb
commit
a794ff40c8
@ -2,6 +2,9 @@
|
||||
Сериализаторы для модуля закупок.
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
@ -33,6 +36,45 @@ PROBLEM_ITEM_STATUSES = {
|
||||
ProcurementItemExecutionStatus.DEFECTIVE,
|
||||
}
|
||||
|
||||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
|
||||
def _validate_expected_delivery_dates(value):
|
||||
if value in (None, ""):
|
||||
return []
|
||||
if not isinstance(value, list):
|
||||
raise serializers.ValidationError("Ожидаемые даты должны быть списком.")
|
||||
|
||||
result = []
|
||||
for raw in value:
|
||||
item = str(raw or "").strip()
|
||||
if not item:
|
||||
continue
|
||||
if not DATE_RE.fullmatch(item):
|
||||
raise serializers.ValidationError(
|
||||
"Каждая ожидаемая дата должна быть в формате YYYY-MM-DD."
|
||||
)
|
||||
try:
|
||||
parsed = date.fromisoformat(item)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError(
|
||||
"Каждая ожидаемая дата должна быть корректной календарной датой."
|
||||
) from exc
|
||||
result.append(parsed.isoformat())
|
||||
return result
|
||||
|
||||
|
||||
def _expected_delivery_date_values(item):
|
||||
dates = []
|
||||
for raw in item.expected_delivery_dates or []:
|
||||
if not isinstance(raw, str) or not DATE_RE.fullmatch(raw):
|
||||
continue
|
||||
try:
|
||||
dates.append(date.fromisoformat(raw))
|
||||
except ValueError:
|
||||
continue
|
||||
return dates
|
||||
|
||||
|
||||
def _validate_item_quantities(attrs, instance=None):
|
||||
quantity = attrs.get("quantity", getattr(instance, "quantity", None))
|
||||
@ -69,20 +111,29 @@ class ProcurementRequestSummaryMixin(serializers.Serializer):
|
||||
|
||||
@extend_schema_field(serializers.DateField(allow_null=True))
|
||||
def get_next_expected_delivery_date(self, obj):
|
||||
dated_items = [
|
||||
item for item in _request_items(obj)
|
||||
if item.expected_delivery_date
|
||||
]
|
||||
if not dated_items:
|
||||
items = _request_items(obj)
|
||||
all_dates = []
|
||||
pending_dates = []
|
||||
all_items_received = all(
|
||||
item.execution_status == ProcurementItemExecutionStatus.RECEIVED
|
||||
for item in items
|
||||
)
|
||||
for item in items:
|
||||
item_dates = _expected_delivery_date_values(item)
|
||||
if not item_dates:
|
||||
continue
|
||||
all_dates.extend(item_dates)
|
||||
if item.execution_status != ProcurementItemExecutionStatus.RECEIVED:
|
||||
pending_dates.extend(item_dates)
|
||||
|
||||
if not all_dates:
|
||||
return None
|
||||
|
||||
pending_dates = [
|
||||
item.expected_delivery_date for item in dated_items
|
||||
if item.execution_status != ProcurementItemExecutionStatus.RECEIVED
|
||||
]
|
||||
if pending_dates:
|
||||
return min(pending_dates).isoformat()
|
||||
return max(item.expected_delivery_date for item in dated_items).isoformat()
|
||||
if all_items_received:
|
||||
return max(all_dates).isoformat()
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.IntegerField())
|
||||
def get_items_total_count(self, obj):
|
||||
@ -141,6 +192,11 @@ class ProcurementItemSerializer(serializers.ModelSerializer):
|
||||
allow_null=True,
|
||||
min_value=0,
|
||||
)
|
||||
expected_delivery_dates = serializers.ListField(
|
||||
child=serializers.CharField(allow_blank=False, trim_whitespace=True),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
)
|
||||
initial_comment = serializers.CharField(
|
||||
write_only=True,
|
||||
required=False,
|
||||
@ -181,7 +237,7 @@ class ProcurementItemSerializer(serializers.ModelSerializer):
|
||||
"received_quantity",
|
||||
"supplier_info",
|
||||
"links",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"execution_status_display",
|
||||
@ -202,6 +258,9 @@ class ProcurementItemSerializer(serializers.ModelSerializer):
|
||||
def validate(self, attrs):
|
||||
return _validate_item_quantities(attrs, self.instance)
|
||||
|
||||
def validate_expected_delivery_dates(self, value):
|
||||
return _validate_expected_delivery_dates(value)
|
||||
|
||||
def create(self, validated_data):
|
||||
initial_comment = validated_data.pop("initial_comment", "").strip()
|
||||
item = super().create(validated_data)
|
||||
@ -235,6 +294,11 @@ class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||||
allow_null=True,
|
||||
min_value=0,
|
||||
)
|
||||
expected_delivery_dates = serializers.ListField(
|
||||
child=serializers.CharField(allow_blank=False, trim_whitespace=True),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
)
|
||||
initial_comment = serializers.CharField(
|
||||
write_only=True,
|
||||
required=False,
|
||||
@ -253,7 +317,7 @@ class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||||
"received_quantity",
|
||||
"supplier_info",
|
||||
"links",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"executor_comment",
|
||||
@ -263,6 +327,9 @@ class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||||
def validate(self, attrs):
|
||||
return _validate_item_quantities(attrs, self.instance)
|
||||
|
||||
def validate_expected_delivery_dates(self, value):
|
||||
return _validate_expected_delivery_dates(value)
|
||||
|
||||
|
||||
class ProcurementItemEditSerializer(ProcurementItemCreateSerializer):
|
||||
"""Сериализатор для редактирования позиций внутри заявки."""
|
||||
|
||||
@ -836,7 +836,7 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
|
||||
"ordered_quantity",
|
||||
"received_quantity",
|
||||
"links",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"executor_comment",
|
||||
|
||||
@ -30,7 +30,7 @@ class ProcurementItemInline(admin.TabularInline):
|
||||
"ordered_quantity",
|
||||
"received_quantity",
|
||||
"execution_status",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"supplier_info",
|
||||
]
|
||||
|
||||
@ -188,7 +188,7 @@ class ProcurementItemAdmin(admin.ModelAdmin):
|
||||
"ordered_quantity",
|
||||
"received_quantity",
|
||||
"execution_status",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"total_price",
|
||||
]
|
||||
list_filter = ["unit", "execution_status", "created_at"]
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
# Generated by Codex on 2026-06-01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
ProcurementItem = apps.get_model("procurement", "ProcurementItem")
|
||||
for item in ProcurementItem.objects.all().iterator():
|
||||
if item.expected_delivery_date:
|
||||
item.expected_delivery_dates = [item.expected_delivery_date.isoformat()]
|
||||
else:
|
||||
item.expected_delivery_dates = []
|
||||
item.save(update_fields=["expected_delivery_dates"])
|
||||
|
||||
|
||||
def backwards(apps, schema_editor):
|
||||
ProcurementItem = apps.get_model("procurement", "ProcurementItem")
|
||||
for item in ProcurementItem.objects.all().iterator():
|
||||
dates = item.expected_delivery_dates or []
|
||||
item.expected_delivery_date = dates[0] if dates else None
|
||||
item.save(update_fields=["expected_delivery_date"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("procurement", "0011_item_quantities_defective_optional_price"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="expected_delivery_dates",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Список ожидаемых дат поступления в формате YYYY-MM-DD",
|
||||
verbose_name="Ожидаемые даты поступления",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(forwards, backwards),
|
||||
migrations.RemoveField(
|
||||
model_name="procurementitem",
|
||||
name="expected_delivery_date",
|
||||
),
|
||||
]
|
||||
@ -289,10 +289,11 @@ class ProcurementItem(models.Model):
|
||||
blank=True,
|
||||
help_text="Список ссылок на позицию",
|
||||
)
|
||||
expected_delivery_date = models.DateField(
|
||||
"Ожидаемая дата поступления",
|
||||
null=True,
|
||||
expected_delivery_dates = models.JSONField(
|
||||
"Ожидаемые даты поступления",
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text="Список ожидаемых дат поступления в формате YYYY-MM-DD",
|
||||
)
|
||||
actual_unit_price = models.DecimalField(
|
||||
"Фактическая цена за единицу",
|
||||
|
||||
@ -294,6 +294,7 @@ class TestProcurementRequestCreate:
|
||||
'unit': 'шт',
|
||||
'estimated_unit_price': '25000.00',
|
||||
'description': '4K, IPS',
|
||||
'expected_delivery_dates': ['2026-06-10', '2026-06-12'],
|
||||
},
|
||||
{
|
||||
'name': 'Монитор 24"',
|
||||
@ -315,6 +316,10 @@ class TestProcurementRequestCreate:
|
||||
request = ProcurementRequest.objects.get(id=request_id)
|
||||
assert request.items.count() == 2
|
||||
assert request.total_cost == Decimal('305000.00') # 5*25k + 10*18k
|
||||
assert request.items.get(name='Монитор 27"').expected_delivery_dates == [
|
||||
'2026-06-10',
|
||||
'2026-06-12',
|
||||
]
|
||||
|
||||
def test_create_request_for_other_department(
|
||||
self, api_client, user, db
|
||||
@ -1595,13 +1600,15 @@ class TestProcessingDepartmentWorkflow:
|
||||
{
|
||||
'execution_status': ProcurementItemExecutionStatus.ORDERED,
|
||||
'actual_unit_price': '120.00',
|
||||
'expected_delivery_date': '2026-05-25',
|
||||
'expected_delivery_dates': ['2026-05-25', '2026-05-29'],
|
||||
'links': ['https://example.com/item'],
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["expected_delivery_dates"] == ["2026-05-25", "2026-05-29"]
|
||||
assert "expected_delivery_date" not in response.data
|
||||
item_notifications = [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_item_updated"
|
||||
@ -1612,11 +1619,63 @@ class TestProcessingDepartmentWorkflow:
|
||||
assert sorted(item_notifications[0]["data"]["changed_fields"]) == [
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"expected_delivery_date",
|
||||
"expected_delivery_dates",
|
||||
"links",
|
||||
"ordered_quantity",
|
||||
]
|
||||
|
||||
def test_item_expected_delivery_dates_must_be_valid_dates(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
item = procurement_item_factory(request=processing_request)
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
item_url = reverse(
|
||||
'api:v1:procurement:procurementitem-detail',
|
||||
kwargs={'pk': item.id},
|
||||
)
|
||||
|
||||
response = api_client.patch(
|
||||
item_url,
|
||||
{'expected_delivery_dates': ['2026-02-31']},
|
||||
format='json',
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_item_expected_delivery_dates_accepts_empty_list(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
item = procurement_item_factory(
|
||||
request=processing_request,
|
||||
expected_delivery_dates=["2026-05-25"],
|
||||
)
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
item_url = reverse(
|
||||
'api:v1:procurement:procurementitem-detail',
|
||||
kwargs={'pk': item.id},
|
||||
)
|
||||
|
||||
response = api_client.patch(
|
||||
item_url,
|
||||
{'expected_delivery_dates': []},
|
||||
format='json',
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["expected_delivery_dates"] == []
|
||||
item.refresh_from_db()
|
||||
assert item.expected_delivery_dates == []
|
||||
|
||||
def test_item_quantities_cannot_exceed_requested_quantity(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
@ -2124,7 +2183,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
execution_status=ProcurementItemExecutionStatus.RECEIVED,
|
||||
ordered_quantity=5,
|
||||
received_quantity=3,
|
||||
expected_delivery_date="2026-05-28",
|
||||
expected_delivery_dates=["2026-05-28"],
|
||||
)
|
||||
procurement_item_factory(
|
||||
request=processing_request,
|
||||
@ -2132,7 +2191,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
quantity=2,
|
||||
execution_status=ProcurementItemExecutionStatus.DEFECTIVE,
|
||||
ordered_quantity=1,
|
||||
expected_delivery_date="2026-05-27",
|
||||
expected_delivery_dates=["2026-05-31", "2026-05-27"],
|
||||
)
|
||||
procurement_item_factory(
|
||||
request=processing_request,
|
||||
@ -2170,7 +2229,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
execution_status=ProcurementItemExecutionStatus.RECEIVED,
|
||||
ordered_quantity=1,
|
||||
received_quantity=1,
|
||||
expected_delivery_date="2026-05-28",
|
||||
expected_delivery_dates=["2026-05-28"],
|
||||
)
|
||||
procurement_item_factory(
|
||||
request=processing_request,
|
||||
@ -2179,7 +2238,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
execution_status=ProcurementItemExecutionStatus.RECEIVED,
|
||||
ordered_quantity=1,
|
||||
received_quantity=1,
|
||||
expected_delivery_date="2026-05-30",
|
||||
expected_delivery_dates=["2026-05-30", "2026-06-01"],
|
||||
)
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse('api:v1:procurement:procurementrequest-list')
|
||||
@ -2191,7 +2250,37 @@ class TestProcessingDepartmentWorkflow:
|
||||
item for item in response.data["results"]
|
||||
if item["id"] == processing_request.id
|
||||
)
|
||||
assert result["next_expected_delivery_date"] == "2026-05-30"
|
||||
assert result["next_expected_delivery_date"] == "2026-06-01"
|
||||
|
||||
def test_list_expected_date_is_null_when_pending_items_have_no_dates(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(
|
||||
request=processing_request,
|
||||
quantity=1,
|
||||
execution_status=ProcurementItemExecutionStatus.RECEIVED,
|
||||
ordered_quantity=1,
|
||||
received_quantity=1,
|
||||
expected_delivery_dates=["2026-05-28"],
|
||||
)
|
||||
procurement_item_factory(
|
||||
request=processing_request,
|
||||
name="Без ожидаемой даты",
|
||||
quantity=1,
|
||||
execution_status=ProcurementItemExecutionStatus.PENDING,
|
||||
)
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse('api:v1:procurement:procurementrequest-list')
|
||||
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = next(
|
||||
item for item in response.data["results"]
|
||||
if item["id"] == processing_request.id
|
||||
)
|
||||
assert result["next_expected_delivery_date"] is None
|
||||
|
||||
def test_requestor_item_update_does_not_notify_self(
|
||||
self, api_client, user, processing_request,
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.db import connection
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db(transaction=True)
|
||||
|
||||
|
||||
def _migrate_to(target):
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.loader.build_graph()
|
||||
executor.migrate(target)
|
||||
return executor.loader.project_state(target).apps
|
||||
|
||||
|
||||
def _migrate_to_latest():
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.loader.build_graph()
|
||||
executor.migrate(executor.loader.graph.leaf_nodes())
|
||||
|
||||
|
||||
def test_expected_delivery_date_migrates_to_dates_list():
|
||||
try:
|
||||
old_apps = _migrate_to(
|
||||
[("procurement", "0011_item_quantities_defective_optional_price")]
|
||||
)
|
||||
Department = old_apps.get_model("employees", "Department")
|
||||
Employee = old_apps.get_model("employees", "Employee")
|
||||
ProcurementRequest = old_apps.get_model("procurement", "ProcurementRequest")
|
||||
ProcurementItem = old_apps.get_model("procurement", "ProcurementItem")
|
||||
|
||||
department = Department.objects.create(name="Migration procurement")
|
||||
employee = Employee.objects.create(
|
||||
email="procurement-migration@example.com",
|
||||
phone_number="+79990001001",
|
||||
first_name="Migration",
|
||||
last_name="Procurement",
|
||||
is_active=True,
|
||||
email_verified=True,
|
||||
)
|
||||
procurement_request = ProcurementRequest.objects.create(
|
||||
title="Migration request",
|
||||
description="Migration request description",
|
||||
department=department,
|
||||
requestor=employee,
|
||||
)
|
||||
item_with_date = ProcurementItem.objects.create(
|
||||
request=procurement_request,
|
||||
name="With date",
|
||||
quantity=1,
|
||||
unit="шт",
|
||||
expected_delivery_date=date(2026, 5, 25),
|
||||
)
|
||||
item_without_date = ProcurementItem.objects.create(
|
||||
request=procurement_request,
|
||||
name="Without date",
|
||||
quantity=1,
|
||||
unit="шт",
|
||||
)
|
||||
|
||||
new_apps = _migrate_to([("procurement", "0012_expected_delivery_dates")])
|
||||
MigratedItem = new_apps.get_model("procurement", "ProcurementItem")
|
||||
|
||||
migrated_with_date = MigratedItem.objects.get(id=item_with_date.id)
|
||||
migrated_without_date = MigratedItem.objects.get(id=item_without_date.id)
|
||||
|
||||
assert migrated_with_date.expected_delivery_dates == ["2026-05-25"]
|
||||
assert migrated_without_date.expected_delivery_dates == []
|
||||
assert not any(
|
||||
field.name == "expected_delivery_date"
|
||||
for field in MigratedItem._meta.get_fields()
|
||||
)
|
||||
finally:
|
||||
_migrate_to_latest()
|
||||
@ -39,7 +39,7 @@ interface ProcurementRequestDetailContentProps {
|
||||
|
||||
type ItemProcessingDraft = {
|
||||
execution_status: ProcurementItemExecutionStatus;
|
||||
expected_delivery_date: string;
|
||||
expected_delivery_dates: string[];
|
||||
actual_unit_price: string;
|
||||
links: string[];
|
||||
ordered_quantity: string;
|
||||
@ -81,6 +81,16 @@ const toOptionalInteger = (value: string): number | null => {
|
||||
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : null;
|
||||
};
|
||||
|
||||
const toDateRows = (dates?: string[] | null): string[] => (
|
||||
Array.isArray(dates)
|
||||
? dates.map((date) => String(date || "").trim()).filter(Boolean)
|
||||
: []
|
||||
);
|
||||
|
||||
const cleanDateRows = (dates: string[]): string[] => (
|
||||
dates.map((date) => date.trim()).filter(Boolean)
|
||||
);
|
||||
|
||||
const approvalIconByStatus = (status?: string) => {
|
||||
const normalized = String(status || "").toLowerCase();
|
||||
if (normalized === "approved") {
|
||||
@ -102,7 +112,7 @@ const initialsFromName = (name: string) =>
|
||||
|
||||
const normalizeItemDraft = (item: ProcurementItem): ItemProcessingDraft => ({
|
||||
execution_status: item.execution_status || "pending",
|
||||
expected_delivery_date: item.expected_delivery_date || "",
|
||||
expected_delivery_dates: toDateRows(item.expected_delivery_dates),
|
||||
actual_unit_price: item.actual_unit_price ? String(item.actual_unit_price) : "",
|
||||
links: toLinkRows(item.links),
|
||||
ordered_quantity: item.ordered_quantity === null || item.ordered_quantity === undefined ? "" : String(item.ordered_quantity),
|
||||
@ -167,6 +177,7 @@ function ProcurementItemCard({
|
||||
const [draft, setDraft] = useState<ItemProcessingDraft>(() => normalizeItemDraft(item));
|
||||
const [processingOpen, setProcessingOpen] = useState(false);
|
||||
const links = Array.isArray(item.links) ? item.links.filter(Boolean) : [];
|
||||
const expectedDeliveryDates = toDateRows(item.expected_delivery_dates);
|
||||
const commentsTotal = item.comments_count ?? comments.length;
|
||||
const requestedQuantity = Math.max(0, Math.trunc(toNumber(item.quantity)));
|
||||
const orderedQuantity = item.ordered_quantity ?? 0;
|
||||
@ -215,7 +226,7 @@ function ProcurementItemCard({
|
||||
const saveItemProcessing = () => {
|
||||
return onUpdateItem?.(requestId, item.id, {
|
||||
execution_status: draft.execution_status,
|
||||
expected_delivery_date: draft.expected_delivery_date || null,
|
||||
expected_delivery_dates: cleanDateRows(draft.expected_delivery_dates),
|
||||
actual_unit_price: draft.actual_unit_price || null,
|
||||
ordered_quantity: toOptionalInteger(draft.ordered_quantity),
|
||||
received_quantity: toOptionalInteger(draft.received_quantity),
|
||||
@ -251,10 +262,18 @@ function ProcurementItemCard({
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="app-text-muted text-[11px]">Дата поступления</p>
|
||||
<p className="app-text-wrap mt-0.5 font-medium text-[var(--foreground)]">
|
||||
{formatDate(item.expected_delivery_date) || "—"}
|
||||
</p>
|
||||
<p className="app-text-muted text-[11px]">Ожидаемые даты</p>
|
||||
{expectedDeliveryDates.length > 0 ? (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{expectedDeliveryDates.map((date, dateIndex) => (
|
||||
<span key={`${item.id}-date-${dateIndex}`} className="app-badge px-2 py-0.5 text-[11px] font-medium">
|
||||
{formatDate(date)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="app-text-wrap mt-0.5 font-medium text-[var(--foreground)]">—</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="app-text-muted text-[11px]">Заказано</p>
|
||||
@ -448,13 +467,44 @@ function ProcurementItemCard({
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="app-text-muted mb-1 block text-[11px] font-medium">Ожидаемая дата</label>
|
||||
<input
|
||||
type="date"
|
||||
value={draft.expected_delivery_date}
|
||||
onChange={(event) => updateDraft({ expected_delivery_date: event.target.value })}
|
||||
className="app-input w-full rounded-lg px-3 py-2 text-xs"
|
||||
/>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<label className="app-text-muted block text-[11px] font-medium">Ожидаемые даты</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateDraft({ expected_delivery_dates: ["", ...draft.expected_delivery_dates] })}
|
||||
className="app-link-accent inline-flex items-center gap-1 text-[11px] font-medium"
|
||||
>
|
||||
<Plus size={12} /> Добавить дату
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{draft.expected_delivery_dates.map((date, dateIndex) => (
|
||||
<div key={dateIndex} className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(event) => updateDraft({
|
||||
expected_delivery_dates: draft.expected_delivery_dates.map((currentDate, currentIndex) => (
|
||||
currentIndex === dateIndex ? event.target.value : currentDate
|
||||
)),
|
||||
})}
|
||||
className="app-input min-w-0 flex-1 rounded-lg px-3 py-2 text-xs"
|
||||
/>
|
||||
{draft.expected_delivery_dates.length > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateDraft({
|
||||
expected_delivery_dates: draft.expected_delivery_dates.filter((_, currentIndex) => currentIndex !== dateIndex),
|
||||
})}
|
||||
className="app-action-secondary inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
||||
title="Удалить дату"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="app-text-muted mb-1 block text-[11px] font-medium">Фактическая цена/ед.</label>
|
||||
@ -690,7 +740,7 @@ export function ProcurementRequestDetailContent({
|
||||
key={[
|
||||
item.id,
|
||||
item.execution_status || "",
|
||||
item.expected_delivery_date || "",
|
||||
Array.isArray(item.expected_delivery_dates) ? item.expected_delivery_dates.join("|") : "",
|
||||
item.actual_unit_price || "",
|
||||
item.ordered_quantity ?? "",
|
||||
item.received_quantity ?? "",
|
||||
|
||||
@ -541,7 +541,7 @@ export interface ProcurementItem {
|
||||
total_price?: string | number;
|
||||
supplier_info?: string;
|
||||
links?: string[];
|
||||
expected_delivery_date?: string | null;
|
||||
expected_delivery_dates?: string[];
|
||||
actual_unit_price?: string | number | null;
|
||||
execution_status?: ProcurementItemExecutionStatus;
|
||||
execution_status_display?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user