diff --git a/app/schemas/box.py b/app/schemas/box.py index 35750e6..f9fe883 100644 --- a/app/schemas/box.py +++ b/app/schemas/box.py @@ -5,45 +5,64 @@ from pydantic import BaseModel, Field, field_validator ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$") +class PendingAccessory(BaseModel): + """待装箱的配件""" + + accessory_id: int + accessory_type: str + quantity: int + location_code: str | None = None + + 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 + item_type: str = "product" class CurrentZongpaiBox(BaseModel): """当前总排号已经分配的箱号明细""" + box_item_id: int box_no: int quantity: int + item_type: str = "product" class BoxDetail(BaseModel): """一个箱号的完整信息""" + box_no: int items: list[BoxItemDetail] class BoxInfoResponse(BaseModel): """GET /box/info 响应""" + zongpai_no: str paichan_no: str work_order_no: str | None = None quantity: int current_zongpai_boxes: list[CurrentZongpaiBox] = Field(default_factory=list) existing_boxes: list[BoxDetail] + pending_accessories: list[PendingAccessory] = Field(default_factory=list) max_box_no: int suggested_box_no: int class BoxSaveRequest(BaseModel): """POST /box 请求""" + zongpai_no: str box_no: int quantity: int + item_type: str = "product" + accessory_id: int | None = None @field_validator("zongpai_no") @classmethod @@ -70,16 +89,19 @@ class BoxSaveRequest(BaseModel): class BoxSaveResponse(BaseModel): """POST /box 响应""" + box_item_id: int paichan_no: str box_no: int zongpai_no: str quantity: int + item_type: str = "product" created_at: str class BoxUpdateRequest(BaseModel): """PATCH /box/{box_item_id} 请求""" + box_no: int quantity: int @@ -100,6 +122,7 @@ class BoxUpdateRequest(BaseModel): class BoxUpdateResponse(BaseModel): """PATCH /box/{box_item_id} 响应""" + box_item_id: int paichan_no: str box_no: int @@ -110,5 +133,6 @@ class BoxUpdateResponse(BaseModel): class BoxDeleteResponse(BaseModel): """DELETE /box/{box_item_id} 响应""" + box_item_id: int deleted: bool diff --git a/app/services/box_service.py b/app/services/box_service.py index 4a8db63..da2b3ba 100644 --- a/app/services/box_service.py +++ b/app/services/box_service.py @@ -3,6 +3,7 @@ import re from sqlalchemy import bindparam, text from sqlalchemy.orm import Session +from app.models.accessory import FinishedGoodsAccessory from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem from app.schemas.box import BoxSaveRequest, BoxUpdateRequest @@ -132,31 +133,68 @@ def get_box_info(db: Session, zongpai_no: str) -> dict: items = box_items[box.id] for item in items: if item.zongpai_no == zongpai_no: - current_zongpai_boxes.append({ - "box_item_id": item.id, - "box_no": box.box_no, - "quantity": int(item.quantity or 0), - }) - existing_boxes.append({ - "box_no": box.box_no, - "items": [ - { - "box_item_id": item.id, - "zongpai_no": item.zongpai_no, - "work_order_no": erp_item_info_map.get( - item.zongpai_no, {} - ).get("work_order_no"), - "quantity": int(item.quantity or 0), - "total_quantity": erp_item_info_map.get( - item.zongpai_no, {} - ).get("total_quantity"), - } - for item in items - ], - }) + current_zongpai_boxes.append( + { + "box_item_id": item.id, + "box_no": box.box_no, + "quantity": int(item.quantity or 0), + "item_type": item.item_type or "product", + } + ) + existing_boxes.append( + { + "box_no": box.box_no, + "items": [ + { + "box_item_id": item.id, + "zongpai_no": item.zongpai_no, + "work_order_no": erp_item_info_map.get(item.zongpai_no, {}).get( + "work_order_no" + ), + "quantity": int(item.quantity or 0), + "total_quantity": erp_item_info_map.get( + item.zongpai_no, {} + ).get("total_quantity"), + "item_type": item.item_type or "product", + } + for item in items + ], + } + ) if box.box_no > max_box_no: max_box_no = box.box_no + # Query pending accessories for this zongpai_no + pending_accessories = [] + accessory_records = ( + db.query(FinishedGoodsAccessory) + .filter( + FinishedGoodsAccessory.zongpai_no == zongpai_no, + FinishedGoodsAccessory.location_code.isnot(None), + ) + .all() + ) + for acc in accessory_records: + is_boxed = ( + db.query(FinishedGoodsBoxItem) + .join(FinishedGoodsBox, FinishedGoodsBoxItem.box_id == FinishedGoodsBox.id) + .filter( + FinishedGoodsBoxItem.zongpai_no == acc.zongpai_no, + FinishedGoodsBox.paichan_no == acc.paichan_no, + FinishedGoodsBoxItem.item_type == "accessory", + ) + .first() + ) is not None + if not is_boxed: + pending_accessories.append( + { + "accessory_id": acc.id, + "accessory_type": acc.accessory_type, + "quantity": acc.quantity, + "location_code": acc.location_code, + } + ) + return { "zongpai_no": zongpai_no, "paichan_no": paichan_no, @@ -164,6 +202,7 @@ def get_box_info(db: Session, zongpai_no: str) -> dict: "quantity": quantity, "current_zongpai_boxes": current_zongpai_boxes, "existing_boxes": existing_boxes, + "pending_accessories": pending_accessories, "max_box_no": max_box_no, "suggested_box_no": max_box_no + 1, } @@ -208,8 +247,19 @@ def _ensure_quantity_within_erp_total( def save_box_record(db: Session, req: BoxSaveRequest) -> dict: """保存装箱记录。同箱可凑箱,但同箱同总排不可重复。""" - erp = query_erp_info(db, req.zongpai_no) - paichan_no = erp["paichan_no"] + if req.item_type == "accessory": + # For accessories, skip ERP quantity validation + accessory = ( + db.query(FinishedGoodsAccessory) + .filter(FinishedGoodsAccessory.id == req.accessory_id) + .first() + ) + if accessory is None: + raise BoxItemNotFoundError() + paichan_no = accessory.paichan_no + else: + erp = query_erp_info(db, req.zongpai_no) + paichan_no = erp["paichan_no"] box = ( db.query(FinishedGoodsBox) @@ -232,13 +282,14 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict: if existing_item: raise DuplicateBoxItemError(paichan_no, req.box_no) - _ensure_quantity_within_erp_total( - db, - paichan_no, - req.zongpai_no, - erp["quantity"], - req.quantity, - ) + if req.item_type != "accessory": + _ensure_quantity_within_erp_total( + db, + paichan_no, + req.zongpai_no, + erp["quantity"], + req.quantity, + ) if box is None: box = FinishedGoodsBox( @@ -252,6 +303,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict: box_id=box.id, zongpai_no=req.zongpai_no, quantity=req.quantity, + item_type=req.item_type, ) db.add(item) db.commit() @@ -263,6 +315,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict: "box_no": req.box_no, "zongpai_no": req.zongpai_no, "quantity": req.quantity, + "item_type": req.item_type, "created_at": item.created_at.isoformat(), } @@ -278,9 +331,7 @@ def update_box_item(db: Session, box_item_id: int, req: BoxUpdateRequest) -> dic raise BoxItemNotFoundError() current_box = ( - db.query(FinishedGoodsBox) - .filter(FinishedGoodsBox.id == item.box_id) - .first() + db.query(FinishedGoodsBox).filter(FinishedGoodsBox.id == item.box_id).first() ) if current_box is None: raise InvalidBoxItemError()