feat(pl): 现场回执提交显式写入出入库单状态
删除首次提交自动改为已完成的隐式逻辑,改为由客户端传入 status。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2474449448
commit
664acff1f3
4 files changed
+136
-2
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user