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})$")
|
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):
|
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
|
||||||
|
item_type: str = "product"
|
||||||
|
|
||||||
|
|
||||||
class CurrentZongpaiBox(BaseModel):
|
class CurrentZongpaiBox(BaseModel):
|
||||||
"""当前总排号已经分配的箱号明细"""
|
"""当前总排号已经分配的箱号明细"""
|
||||||
|
|
||||||
box_item_id: int
|
box_item_id: int
|
||||||
box_no: int
|
box_no: int
|
||||||
quantity: int
|
quantity: int
|
||||||
|
item_type: str = "product"
|
||||||
|
|
||||||
|
|
||||||
class BoxDetail(BaseModel):
|
class BoxDetail(BaseModel):
|
||||||
"""一个箱号的完整信息"""
|
"""一个箱号的完整信息"""
|
||||||
|
|
||||||
box_no: int
|
box_no: int
|
||||||
items: list[BoxItemDetail]
|
items: list[BoxItemDetail]
|
||||||
|
|
||||||
|
|
||||||
class BoxInfoResponse(BaseModel):
|
class BoxInfoResponse(BaseModel):
|
||||||
"""GET /box/info 响应"""
|
"""GET /box/info 响应"""
|
||||||
|
|
||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
paichan_no: str
|
paichan_no: str
|
||||||
work_order_no: str | None = None
|
work_order_no: str | None = None
|
||||||
quantity: int
|
quantity: int
|
||||||
current_zongpai_boxes: list[CurrentZongpaiBox] = Field(default_factory=list)
|
current_zongpai_boxes: list[CurrentZongpaiBox] = Field(default_factory=list)
|
||||||
existing_boxes: list[BoxDetail]
|
existing_boxes: list[BoxDetail]
|
||||||
|
pending_accessories: list[PendingAccessory] = Field(default_factory=list)
|
||||||
max_box_no: int
|
max_box_no: int
|
||||||
suggested_box_no: int
|
suggested_box_no: int
|
||||||
|
|
||||||
|
|
||||||
class BoxSaveRequest(BaseModel):
|
class BoxSaveRequest(BaseModel):
|
||||||
"""POST /box 请求"""
|
"""POST /box 请求"""
|
||||||
|
|
||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
box_no: int
|
box_no: int
|
||||||
quantity: int
|
quantity: int
|
||||||
|
item_type: str = "product"
|
||||||
|
accessory_id: int | None = None
|
||||||
|
|
||||||
@field_validator("zongpai_no")
|
@field_validator("zongpai_no")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -70,16 +89,19 @@ class BoxSaveRequest(BaseModel):
|
|||||||
|
|
||||||
class BoxSaveResponse(BaseModel):
|
class BoxSaveResponse(BaseModel):
|
||||||
"""POST /box 响应"""
|
"""POST /box 响应"""
|
||||||
|
|
||||||
box_item_id: int
|
box_item_id: int
|
||||||
paichan_no: str
|
paichan_no: str
|
||||||
box_no: int
|
box_no: int
|
||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
quantity: int
|
quantity: int
|
||||||
|
item_type: str = "product"
|
||||||
created_at: str
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
class BoxUpdateRequest(BaseModel):
|
class BoxUpdateRequest(BaseModel):
|
||||||
"""PATCH /box/{box_item_id} 请求"""
|
"""PATCH /box/{box_item_id} 请求"""
|
||||||
|
|
||||||
box_no: int
|
box_no: int
|
||||||
quantity: int
|
quantity: int
|
||||||
|
|
||||||
@@ -100,6 +122,7 @@ class BoxUpdateRequest(BaseModel):
|
|||||||
|
|
||||||
class BoxUpdateResponse(BaseModel):
|
class BoxUpdateResponse(BaseModel):
|
||||||
"""PATCH /box/{box_item_id} 响应"""
|
"""PATCH /box/{box_item_id} 响应"""
|
||||||
|
|
||||||
box_item_id: int
|
box_item_id: int
|
||||||
paichan_no: str
|
paichan_no: str
|
||||||
box_no: int
|
box_no: int
|
||||||
@@ -110,5 +133,6 @@ class BoxUpdateResponse(BaseModel):
|
|||||||
|
|
||||||
class BoxDeleteResponse(BaseModel):
|
class BoxDeleteResponse(BaseModel):
|
||||||
"""DELETE /box/{box_item_id} 响应"""
|
"""DELETE /box/{box_item_id} 响应"""
|
||||||
|
|
||||||
box_item_id: int
|
box_item_id: int
|
||||||
deleted: bool
|
deleted: bool
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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.accessory import FinishedGoodsAccessory
|
||||||
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
|
||||||
|
|
||||||
@@ -132,31 +133,68 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
|
|||||||
items = box_items[box.id]
|
items = box_items[box.id]
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.zongpai_no == zongpai_no:
|
if item.zongpai_no == zongpai_no:
|
||||||
current_zongpai_boxes.append({
|
current_zongpai_boxes.append(
|
||||||
"box_item_id": item.id,
|
{
|
||||||
"box_no": box.box_no,
|
"box_item_id": item.id,
|
||||||
"quantity": int(item.quantity or 0),
|
"box_no": box.box_no,
|
||||||
})
|
"quantity": int(item.quantity or 0),
|
||||||
existing_boxes.append({
|
"item_type": item.item_type or "product",
|
||||||
"box_no": box.box_no,
|
}
|
||||||
"items": [
|
)
|
||||||
{
|
existing_boxes.append(
|
||||||
"box_item_id": item.id,
|
{
|
||||||
"zongpai_no": item.zongpai_no,
|
"box_no": box.box_no,
|
||||||
"work_order_no": erp_item_info_map.get(
|
"items": [
|
||||||
item.zongpai_no, {}
|
{
|
||||||
).get("work_order_no"),
|
"box_item_id": item.id,
|
||||||
"quantity": int(item.quantity or 0),
|
"zongpai_no": item.zongpai_no,
|
||||||
"total_quantity": erp_item_info_map.get(
|
"work_order_no": erp_item_info_map.get(item.zongpai_no, {}).get(
|
||||||
item.zongpai_no, {}
|
"work_order_no"
|
||||||
).get("total_quantity"),
|
),
|
||||||
}
|
"quantity": int(item.quantity or 0),
|
||||||
for item in items
|
"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:
|
if box.box_no > max_box_no:
|
||||||
max_box_no = box.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 {
|
return {
|
||||||
"zongpai_no": zongpai_no,
|
"zongpai_no": zongpai_no,
|
||||||
"paichan_no": paichan_no,
|
"paichan_no": paichan_no,
|
||||||
@@ -164,6 +202,7 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
|
|||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
"current_zongpai_boxes": current_zongpai_boxes,
|
"current_zongpai_boxes": current_zongpai_boxes,
|
||||||
"existing_boxes": existing_boxes,
|
"existing_boxes": existing_boxes,
|
||||||
|
"pending_accessories": pending_accessories,
|
||||||
"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,
|
||||||
}
|
}
|
||||||
@@ -208,8 +247,19 @@ def _ensure_quantity_within_erp_total(
|
|||||||
|
|
||||||
def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||||
"""保存装箱记录。同箱可凑箱,但同箱同总排不可重复。"""
|
"""保存装箱记录。同箱可凑箱,但同箱同总排不可重复。"""
|
||||||
erp = query_erp_info(db, req.zongpai_no)
|
if req.item_type == "accessory":
|
||||||
paichan_no = erp["paichan_no"]
|
# 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 = (
|
box = (
|
||||||
db.query(FinishedGoodsBox)
|
db.query(FinishedGoodsBox)
|
||||||
@@ -232,13 +282,14 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
|||||||
if existing_item:
|
if existing_item:
|
||||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||||
|
|
||||||
_ensure_quantity_within_erp_total(
|
if req.item_type != "accessory":
|
||||||
db,
|
_ensure_quantity_within_erp_total(
|
||||||
paichan_no,
|
db,
|
||||||
req.zongpai_no,
|
paichan_no,
|
||||||
erp["quantity"],
|
req.zongpai_no,
|
||||||
req.quantity,
|
erp["quantity"],
|
||||||
)
|
req.quantity,
|
||||||
|
)
|
||||||
|
|
||||||
if box is None:
|
if box is None:
|
||||||
box = FinishedGoodsBox(
|
box = FinishedGoodsBox(
|
||||||
@@ -252,6 +303,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
|||||||
box_id=box.id,
|
box_id=box.id,
|
||||||
zongpai_no=req.zongpai_no,
|
zongpai_no=req.zongpai_no,
|
||||||
quantity=req.quantity,
|
quantity=req.quantity,
|
||||||
|
item_type=req.item_type,
|
||||||
)
|
)
|
||||||
db.add(item)
|
db.add(item)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -263,6 +315,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
|||||||
"box_no": req.box_no,
|
"box_no": req.box_no,
|
||||||
"zongpai_no": req.zongpai_no,
|
"zongpai_no": req.zongpai_no,
|
||||||
"quantity": req.quantity,
|
"quantity": req.quantity,
|
||||||
|
"item_type": req.item_type,
|
||||||
"created_at": item.created_at.isoformat(),
|
"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()
|
raise BoxItemNotFoundError()
|
||||||
|
|
||||||
current_box = (
|
current_box = (
|
||||||
db.query(FinishedGoodsBox)
|
db.query(FinishedGoodsBox).filter(FinishedGoodsBox.id == item.box_id).first()
|
||||||
.filter(FinishedGoodsBox.id == item.box_id)
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
if current_box is None:
|
if current_box is None:
|
||||||
raise InvalidBoxItemError()
|
raise InvalidBoxItemError()
|
||||||
|
|||||||
Reference in New Issue
Block a user