feat: extend boxing module to support accessory items

- Add PendingAccessory schema and pending_accessories to BoxInfoResponse
- Add item_type field to BoxItemDetail, CurrentZongpaiBox, BoxSaveRequest, BoxSaveResponse
- Query pending (unboxed) accessories in get_box_info
- Branch save_box_record to skip ERP validation for accessory items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-05-24 17:43:15 +08:00
parent 0539c4696d
commit 39b8e5de6a
2 changed files with 109 additions and 34 deletions

View File

@@ -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

View File

@@ -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()