feat(pl): 现场回执提交显式写入出入库单状态

删除首次提交自动改为已完成的隐式逻辑,改为由客户端传入 status。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-19 17:13:40 +08:00
1 parent 2474449448
commit 664acff1f3
4 files changed
+136 -2

No files matched your search

+1
View File
@@ -93,6 +93,7 @@ def _make_handlers(kind: str):
item_qty=qty_map,
remark=payload.remark,
images=images,
status=payload.status,
)
except ValueError as exc:
return error_response(400, str(exc))
+1
View File
@@ -92,6 +92,7 @@ class PublicIoOrderItemFeedback(BaseModel):
class PublicIoOrderFeedback(BaseModel):
status: str
remark: Optional[str] = None
images: list[str] = Field(default_factory=list)
items: list[PublicIoOrderItemFeedback] = Field(default_factory=list)
+7 -2
View File
@@ -304,6 +304,9 @@ def dump_feedback_images(images: list[str]) -> str:
return json.dumps(images, ensure_ascii=False)
_VALID_SITE_FEEDBACK_STATUSES = frozenset({"in_progress", "completed", "voided"})
def apply_site_feedback(
db: Session,
*,
@@ -312,10 +315,13 @@ def apply_site_feedback(
item_qty: dict[str, Decimal],
remark: Optional[str],
images: list[str],
status: str,
user_id: Optional[str] = None,
) -> None:
if header.status == "voided":
raise ValueError("单据已作废,无法提交")
if status not in _VALID_SITE_FEEDBACK_STATUSES:
raise ValueError(f"无效状态: {status}")
now = datetime.now()
for item in items:
if item.id not in item_qty:
@@ -328,8 +334,7 @@ def apply_site_feedback(
header.site_feedback_time = now
if not header.site_first_submitted_at:
header.site_first_submitted_at = now
if header.status == "in_progress":
header.status = "completed"
header.status = status
header.update_by = user_id
db.flush()
@@ -0,0 +1,127 @@
from datetime import datetime
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from app.services.pl_io_order_service import apply_site_feedback
def _header(**kwargs):
defaults = dict(
status="in_progress",
site_feedback_remark=None,
site_feedback_images=None,
site_feedback_time=None,
site_first_submitted_at=None,
update_by=None,
)
defaults.update(kwargs)
return SimpleNamespace(**defaults)
def _item(item_id="item-1"):
return SimpleNamespace(
id=item_id,
actual_qty=None,
qty_source=None,
update_by=None,
)
def test_apply_site_feedback_sets_status_completed():
header = _header()
item = _item()
db = MagicMock()
apply_site_feedback(
db,
header=header,
items=[item],
item_qty={"item-1": Decimal("12.5")},
remark="ok",
images=["a.jpg"],
status="completed",
)
assert header.status == "completed"
assert item.actual_qty == Decimal("12.5")
assert header.site_first_submitted_at is not None
assert header.site_feedback_remark == "ok"
db.flush.assert_called_once()
def test_apply_site_feedback_sets_status_voided():
header = _header()
apply_site_feedback(
MagicMock(),
header=header,
items=[_item()],
item_qty={"item-1": Decimal("1")},
remark=None,
images=[],
status="voided",
)
assert header.status == "voided"
def test_apply_site_feedback_keeps_in_progress_when_requested():
"""首次提交选进行中时,不得隐式改成已完成。"""
header = _header(status="in_progress")
apply_site_feedback(
MagicMock(),
header=header,
items=[_item()],
item_qty={"item-1": Decimal("1")},
remark=None,
images=[],
status="in_progress",
)
assert header.status == "in_progress"
assert header.site_first_submitted_at is not None
def test_apply_site_feedback_rejects_invalid_status():
header = _header()
with pytest.raises(ValueError, match="无效状态"):
apply_site_feedback(
MagicMock(),
header=header,
items=[_item()],
item_qty={},
remark=None,
images=[],
status="done",
)
def test_apply_site_feedback_rejects_already_voided():
header = _header(status="voided")
with pytest.raises(ValueError, match="单据已作废"):
apply_site_feedback(
MagicMock(),
header=header,
items=[_item()],
item_qty={},
remark=None,
images=[],
status="completed",
)
def test_apply_site_feedback_does_not_reset_first_submitted_at():
first = datetime(2026, 1, 1, 12, 0, 0)
header = _header(
status="completed",
site_first_submitted_at=first,
)
apply_site_feedback(
MagicMock(),
header=header,
items=[_item()],
item_qty={"item-1": Decimal("2")},
remark="二改",
images=[],
status="in_progress",
)
assert header.status == "in_progress"
assert header.site_first_submitted_at == first