From 5b2106b30de0f6fcf2d440fa4630dc85726dfb24 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 12 Aug 2026 14:18:56 +0800 Subject: [PATCH] feat(server): extend GET /box/info with mixed items and attachment summary Co-Authored-By: Claude --- app/schemas/box.py | 12 ++++- app/services/box_service.py | 81 ++++++++++++++++++++++++++++++++- tests/test_attachment_boxing.py | 30 ++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/app/schemas/box.py b/app/schemas/box.py index 35750e6..f0edd8a 100644 --- a/app/schemas/box.py +++ b/app/schemas/box.py @@ -6,12 +6,15 @@ ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$") class BoxItemDetail(BaseModel): - """箱号下某个总排号的明细""" + """箱号下某个总排号的明细(产品或附件)""" box_item_id: int | None = None zongpai_no: str work_order_no: str | None = None quantity: int total_quantity: int | None = None + kind: str = "product" + category_id: int | None = None + category_name: str | None = None class CurrentZongpaiBox(BaseModel): @@ -27,6 +30,12 @@ class BoxDetail(BaseModel): items: list[BoxItemDetail] +class AttachmentSummary(BaseModel): + determination: str + all_complete: bool + pending_count: int + + class BoxInfoResponse(BaseModel): """GET /box/info 响应""" zongpai_no: str @@ -37,6 +46,7 @@ class BoxInfoResponse(BaseModel): existing_boxes: list[BoxDetail] max_box_no: int suggested_box_no: int + attachment_summary: AttachmentSummary class BoxSaveRequest(BaseModel): diff --git a/app/services/box_service.py b/app/services/box_service.py index 4a8db63..bca0ba8 100644 --- a/app/services/box_service.py +++ b/app/services/box_service.py @@ -3,6 +3,12 @@ import re from sqlalchemy import bindparam, text from sqlalchemy.orm import Session +from app.models.attachment import ( + AttachmentCategory, + FinishedGoodsAttachment, + FinishedGoodsAttachmentItem, + FinishedGoodsBoxAttachmentItem, +) from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem from app.schemas.box import BoxSaveRequest, BoxUpdateRequest @@ -128,6 +134,41 @@ def get_box_info(db: Session, zongpai_no: str) -> dict: existing_boxes = [] current_zongpai_boxes = [] 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: items = box_items[box.id] 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( item.zongpai_no, {} ).get("total_quantity"), + "kind": "product", } for item in items - ], + ] + items_by_box.get(box.id, []), }) if box.box_no > max_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 { "zongpai_no": zongpai_no, "paichan_no": paichan_no, @@ -166,6 +244,7 @@ def get_box_info(db: Session, zongpai_no: str) -> dict: "existing_boxes": existing_boxes, "max_box_no": max_box_no, "suggested_box_no": max_box_no + 1, + "attachment_summary": attachment_summary, } diff --git a/tests/test_attachment_boxing.py b/tests/test_attachment_boxing.py index f22679e..9d07e34 100644 --- a/tests/test_attachment_boxing.py +++ b/tests/test_attachment_boxing.py @@ -226,3 +226,33 @@ def test_attachment_box_delete_cleans_empty_box(client: TestClient, db: Session) }, ) 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