feat(server): extend GET /box/info with mixed items and attachment summary

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-08-12 14:18:56 +08:00
parent 4d55420958
commit 5b2106b30d
3 changed files with 121 additions and 2 deletions

View File

@@ -6,12 +6,15 @@ ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$")
class BoxItemDetail(BaseModel): class BoxItemDetail(BaseModel):
"""箱号下某个总排号的明细""" """箱号下某个总排号的明细(产品或附件)"""
box_item_id: int | None = None box_item_id: int | None = None
zongpai_no: str zongpai_no: str
work_order_no: str | None = None work_order_no: str | None = None
quantity: int quantity: int
total_quantity: int | None = None total_quantity: int | None = None
kind: str = "product"
category_id: int | None = None
category_name: str | None = None
class CurrentZongpaiBox(BaseModel): class CurrentZongpaiBox(BaseModel):
@@ -27,6 +30,12 @@ class BoxDetail(BaseModel):
items: list[BoxItemDetail] items: list[BoxItemDetail]
class AttachmentSummary(BaseModel):
determination: str
all_complete: bool
pending_count: int
class BoxInfoResponse(BaseModel): class BoxInfoResponse(BaseModel):
"""GET /box/info 响应""" """GET /box/info 响应"""
zongpai_no: str zongpai_no: str
@@ -37,6 +46,7 @@ class BoxInfoResponse(BaseModel):
existing_boxes: list[BoxDetail] existing_boxes: list[BoxDetail]
max_box_no: int max_box_no: int
suggested_box_no: int suggested_box_no: int
attachment_summary: AttachmentSummary
class BoxSaveRequest(BaseModel): class BoxSaveRequest(BaseModel):

View File

@@ -3,6 +3,12 @@ import re
from sqlalchemy import bindparam, text from sqlalchemy import bindparam, text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.attachment import (
AttachmentCategory,
FinishedGoodsAttachment,
FinishedGoodsAttachmentItem,
FinishedGoodsBoxAttachmentItem,
)
from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem
from app.schemas.box import BoxSaveRequest, BoxUpdateRequest from app.schemas.box import BoxSaveRequest, BoxUpdateRequest
@@ -128,6 +134,41 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
existing_boxes = [] existing_boxes = []
current_zongpai_boxes = [] current_zongpai_boxes = []
max_box_no = 0 max_box_no = 0
paichan_box_rows = (
db.query(FinishedGoodsBox)
.filter(FinishedGoodsBox.paichan_no == paichan_no)
.all()
)
att_rows = (
db.query(FinishedGoodsBoxAttachmentItem)
.filter(
FinishedGoodsBoxAttachmentItem.box_id.in_(
[b.id for b in paichan_box_rows]
)
)
.all()
)
att_category_ids = {r.category_id for r in att_rows}
att_names = {
c.id: c.name
for c in db.query(AttachmentCategory)
.filter(AttachmentCategory.id.in_(list(att_category_ids)))
.all()
} if att_category_ids else {}
items_by_box: dict[int, list[dict]] = {}
for r in att_rows:
items_by_box.setdefault(r.box_id, []).append(
{
"box_item_id": r.id,
"zongpai_no": r.zongpai_no,
"work_order_no": None,
"quantity": int(r.quantity or 0),
"total_quantity": None,
"kind": "attachment",
"category_id": r.category_id,
"category_name": att_names.get(r.category_id),
}
)
for box in boxes: for box in boxes:
items = box_items[box.id] items = box_items[box.id]
for item in items: for item in items:
@@ -150,13 +191,50 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
"total_quantity": erp_item_info_map.get( "total_quantity": erp_item_info_map.get(
item.zongpai_no, {} item.zongpai_no, {}
).get("total_quantity"), ).get("total_quantity"),
"kind": "product",
} }
for item in items for item in items
], ] + items_by_box.get(box.id, []),
}) })
if box.box_no > max_box_no: if box.box_no > max_box_no:
max_box_no = box.box_no max_box_no = box.box_no
# Attachment summary for the scanned总排号
att_row = (
db.query(FinishedGoodsAttachment)
.filter(FinishedGoodsAttachment.zongpai_no == zongpai_no)
.first()
)
determination = att_row.determination if att_row else "undetermined"
plan_items = (
db.query(FinishedGoodsAttachmentItem)
.filter(FinishedGoodsAttachmentItem.zongpai_no == zongpai_no)
.all()
)
pending_count = 0
for it in plan_items:
boxed = sum(
int(r.quantity or 0)
for r in db.query(FinishedGoodsBoxAttachmentItem)
.filter(
FinishedGoodsBoxAttachmentItem.zongpai_no == zongpai_no,
FinishedGoodsBoxAttachmentItem.category_id == it.category_id,
)
.all()
)
if boxed < it.expected_qty:
pending_count += 1
all_complete = (
determination == "has"
and len(plan_items) > 0
and pending_count == 0
)
attachment_summary = {
"determination": determination,
"all_complete": all_complete,
"pending_count": pending_count,
}
return { return {
"zongpai_no": zongpai_no, "zongpai_no": zongpai_no,
"paichan_no": paichan_no, "paichan_no": paichan_no,
@@ -166,6 +244,7 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
"existing_boxes": existing_boxes, "existing_boxes": existing_boxes,
"max_box_no": max_box_no, "max_box_no": max_box_no,
"suggested_box_no": max_box_no + 1, "suggested_box_no": max_box_no + 1,
"attachment_summary": attachment_summary,
} }

View File

@@ -226,3 +226,33 @@ def test_attachment_box_delete_cleans_empty_box(client: TestClient, db: Session)
}, },
) )
assert recreated.status_code == 200 assert recreated.status_code == 200
def test_box_info_includes_attachment_summary(client: TestClient, db: Session):
cert_id, extra_id = _seed(db)
client.put(
"/CargoTrace/attachment/config",
json={
"zongpai_no": "26BW0011",
"determination": "has",
"items": [
{"category_id": cert_id, "expected_qty": 80},
{"category_id": extra_id, "expected_qty": 5},
],
},
)
client.post(
"/CargoTrace/attachment/box",
json={
"zongpai_no": "26BW0011",
"category_id": cert_id,
"box_no": 951,
"quantity": 80,
},
)
info = client.get(
"/CargoTrace/box/info", params={"zongpai_no": "26BW0011"}
).json()
assert info["attachment_summary"]["determination"] == "has"
assert info["attachment_summary"]["all_complete"] is False
assert info["attachment_summary"]["pending_count"] == 1 # extra not yet boxed