Allow editing orders in the order card

Cashiers can edit recipient, TTN, notes, goods and total in the order
modal and save via PATCH /orders/{id}. Edited orders get edited_at and
are no longer overwritten by CRM sync. Editing is blocked once a receipt
exists; changing the TTN resets Nova Poshta tracking fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 17:23:41 +03:00
co-authored by Claude Opus 5.5
parent cbf9832e5b
commit 56ac0fc370
16 changed files with 946 additions and 58 deletions
+41
View File
@@ -10,6 +10,7 @@ integer-kopecks из CLAUDE.md относится к будущим персис
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -88,6 +89,7 @@ class OrderRowOut(BaseModel):
receipt_status: str | None = None
receipt_error: str | None = None
receipt_prepayment: str | None = None
edited: bool = False
@classmethod
def from_order(cls, order: Order, receipt: Receipt | None = None) -> OrderRowOut:
@@ -116,4 +118,43 @@ class OrderRowOut(BaseModel):
receipt_prepayment=(
f"{receipt.prepayment_kopecks / 100:.2f}" if receipt else None
),
edited=order.edited_at is not None,
)
class OrderGoodIn(BaseModel):
"""Позиция заказа из формы редактирования. `amount` считает сервер."""
id: str | None = Field(default=None, max_length=64)
sku: str = Field(default="", max_length=128)
name: str = Field(min_length=1, max_length=512)
price: Decimal = Field(ge=0, max_digits=12, decimal_places=2)
quantity: Decimal = Field(gt=0, max_digits=12, decimal_places=3)
discount_amount: Decimal = Field(default=Decimal(0), ge=0, max_digits=12, decimal_places=2)
@field_validator("sku", "name", mode="after")
@classmethod
def _strip(cls, value: str) -> str:
return value.strip()
class OrderUpdateIn(BaseModel):
"""`PATCH /orders/{id}` — полная замена редактируемых полей заказа."""
recipient_name: str | None = Field(default=None, max_length=255)
recipient_phone: str | None = Field(default=None, max_length=32)
recipient_email: str | None = Field(default=None, max_length=320)
waybill_number: str | None = Field(default=None, max_length=64)
notes: str | None = None
total_amount: Decimal = Field(ge=0, max_digits=12, decimal_places=2)
goods: list[OrderGoodIn] = Field(min_length=1)
# Пустая строка из формы = «не указано».
@field_validator(
"recipient_name", "recipient_phone", "recipient_email", "waybill_number", "notes"
)
@classmethod
def _blank_to_none(cls, value: str | None) -> str | None:
if value is None:
return None
return value.strip() or None