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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
current_zongpai_boxes.append(
|
||||
{
|
||||
"box_item_id": item.id,
|
||||
"box_no": box.box_no,
|
||||
"quantity": int(item.quantity or 0),
|
||||
})
|
||||
existing_boxes.append({
|
||||
"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"),
|
||||
"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,6 +247,17 @@ def _ensure_quantity_within_erp_total(
|
||||
|
||||
def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
"""保存装箱记录。同箱可凑箱,但同箱同总排不可重复。"""
|
||||
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"]
|
||||
|
||||
@@ -232,6 +282,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
if existing_item:
|
||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||
|
||||
if req.item_type != "accessory":
|
||||
_ensure_quantity_within_erp_total(
|
||||
db,
|
||||
paichan_no,
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user