feat(box): simplify boxing mode API
This commit is contained in:
@@ -37,7 +37,7 @@ def create_box(req: BoxSaveRequest, db: Session = Depends(get_db)):
|
||||
|
||||
@router.patch("/box/{box_item_id}", response_model=BoxUpdateResponse)
|
||||
def update_box(box_item_id: int, req: BoxUpdateRequest, db: Session = Depends(get_db)):
|
||||
"""更新一码多箱已分配装箱明细。"""
|
||||
"""更新已分配装箱明细。"""
|
||||
return update_box_item(db, box_item_id, req)
|
||||
|
||||
|
||||
|
||||
27
app/main.py
27
app/main.py
@@ -22,8 +22,8 @@ from app.services.location_service import AlreadyOffShelfError, DuplicateLocatio
|
||||
from app.services.box_service import (
|
||||
BoxItemNotFoundError,
|
||||
DuplicateBoxItemError,
|
||||
DuplicateZongpaiBindError,
|
||||
InvalidBoxItemError,
|
||||
InvalidQuantityError,
|
||||
InvalidZongpaiError,
|
||||
ZongpaiNotFoundError,
|
||||
)
|
||||
@@ -126,20 +126,6 @@ async def duplicate_box_handler(request: Request, exc: DuplicateBoxItemError):
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(DuplicateZongpaiBindError)
|
||||
async def duplicate_zongpai_bind_handler(request: Request, exc: DuplicateZongpaiBindError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={
|
||||
"error_code": "DUPLICATE_ZONGPAI_BIND",
|
||||
"message": f"总排号 {exc.zongpai_no} 已绑定箱号 {exc.box_no},请勿重复装箱",
|
||||
"paichan_no": exc.paichan_no,
|
||||
"box_no": exc.box_no,
|
||||
"zongpai_no": exc.zongpai_no,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(BoxItemNotFoundError)
|
||||
async def box_item_not_found_handler(request: Request, exc: BoxItemNotFoundError):
|
||||
return JSONResponse(
|
||||
@@ -151,6 +137,17 @@ async def box_item_not_found_handler(request: Request, exc: BoxItemNotFoundError
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(InvalidQuantityError)
|
||||
async def invalid_quantity_handler(request: Request, exc: InvalidQuantityError):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"error_code": "INVALID_QUANTITY",
|
||||
"message": "装箱数量超出可装数量上限",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(InvalidBoxItemError)
|
||||
async def invalid_box_item_handler(request: Request, exc: InvalidBoxItemError):
|
||||
return JSONResponse(
|
||||
|
||||
@@ -44,7 +44,6 @@ class BoxSaveRequest(BaseModel):
|
||||
zongpai_no: str
|
||||
box_no: int
|
||||
quantity: int
|
||||
box_mode: str | None = None # one-to-one | one-to-many | many-to-one
|
||||
|
||||
@field_validator("zongpai_no")
|
||||
@classmethod
|
||||
@@ -83,7 +82,6 @@ class BoxUpdateRequest(BaseModel):
|
||||
"""PATCH /box/{box_item_id} 请求"""
|
||||
box_no: int
|
||||
quantity: int
|
||||
box_mode: str = "one-to-many"
|
||||
|
||||
@field_validator("box_no")
|
||||
@classmethod
|
||||
|
||||
@@ -23,13 +23,6 @@ class DuplicateBoxItemError(Exception):
|
||||
self.box_no = box_no
|
||||
|
||||
|
||||
class DuplicateZongpaiBindError(Exception):
|
||||
def __init__(self, paichan_no: str, box_no: int, zongpai_no: str):
|
||||
self.paichan_no = paichan_no
|
||||
self.box_no = box_no
|
||||
self.zongpai_no = zongpai_no
|
||||
|
||||
|
||||
class BoxItemNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
@@ -38,6 +31,10 @@ class InvalidBoxItemError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQuantityError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def query_erp_info(db: Session, zongpai_no: str) -> dict:
|
||||
"""从 ERP 视图查询总排号对应的排产号、工令号和数量。"""
|
||||
sql = text(_erp_info_sql(db))
|
||||
@@ -172,24 +169,48 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _packed_quantity_for_zongpai(
|
||||
db: Session,
|
||||
paichan_no: str,
|
||||
zongpai_no: str,
|
||||
exclude_box_item_id: int | None = None,
|
||||
) -> int:
|
||||
query = (
|
||||
db.query(FinishedGoodsBoxItem)
|
||||
.join(FinishedGoodsBox, FinishedGoodsBoxItem.box_id == FinishedGoodsBox.id)
|
||||
.filter(
|
||||
FinishedGoodsBox.paichan_no == paichan_no,
|
||||
FinishedGoodsBoxItem.zongpai_no == zongpai_no,
|
||||
)
|
||||
)
|
||||
if exclude_box_item_id is not None:
|
||||
query = query.filter(FinishedGoodsBoxItem.id != exclude_box_item_id)
|
||||
return sum(int(item.quantity or 0) for item in query.all())
|
||||
|
||||
|
||||
def _ensure_quantity_within_erp_total(
|
||||
db: Session,
|
||||
paichan_no: str,
|
||||
zongpai_no: str,
|
||||
erp_quantity: int,
|
||||
submitted_quantity: int,
|
||||
exclude_box_item_id: int | None = None,
|
||||
) -> None:
|
||||
packed_quantity = _packed_quantity_for_zongpai(
|
||||
db,
|
||||
paichan_no,
|
||||
zongpai_no,
|
||||
exclude_box_item_id=exclude_box_item_id,
|
||||
)
|
||||
if packed_quantity + submitted_quantity > erp_quantity:
|
||||
raise InvalidQuantityError()
|
||||
|
||||
|
||||
def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
"""保存装箱记录。箱已存在时仅追加明细(多码一箱场景)。"""
|
||||
"""保存装箱记录。同箱可凑箱,但同箱同总排不可重复。"""
|
||||
erp = query_erp_info(db, req.zongpai_no)
|
||||
paichan_no = erp["paichan_no"]
|
||||
|
||||
if req.box_mode == "one-to-one":
|
||||
existing_box_no = (
|
||||
db.query(FinishedGoodsBox.box_no)
|
||||
.join(FinishedGoodsBoxItem, FinishedGoodsBox.id == FinishedGoodsBoxItem.box_id)
|
||||
.filter(
|
||||
FinishedGoodsBox.paichan_no == paichan_no,
|
||||
FinishedGoodsBoxItem.zongpai_no == req.zongpai_no,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
if existing_box_no is not None:
|
||||
raise DuplicateZongpaiBindError(paichan_no, existing_box_no, req.zongpai_no)
|
||||
|
||||
box = (
|
||||
db.query(FinishedGoodsBox)
|
||||
.filter(
|
||||
@@ -200,8 +221,6 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
)
|
||||
|
||||
if box:
|
||||
if req.box_mode in ("one-to-one", "one-to-many"):
|
||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||
existing_item = (
|
||||
db.query(FinishedGoodsBoxItem)
|
||||
.filter(
|
||||
@@ -212,7 +231,16 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
)
|
||||
if existing_item:
|
||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||
else:
|
||||
|
||||
_ensure_quantity_within_erp_total(
|
||||
db,
|
||||
paichan_no,
|
||||
req.zongpai_no,
|
||||
erp["quantity"],
|
||||
req.quantity,
|
||||
)
|
||||
|
||||
if box is None:
|
||||
box = FinishedGoodsBox(
|
||||
paichan_no=paichan_no,
|
||||
box_no=req.box_no,
|
||||
@@ -240,7 +268,7 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
||||
|
||||
|
||||
def update_box_item(db: Session, box_item_id: int, req: BoxUpdateRequest) -> dict:
|
||||
"""更新一码多箱已分配明细的箱号和数量。"""
|
||||
"""更新已分配明细的箱号和数量。"""
|
||||
item = (
|
||||
db.query(FinishedGoodsBoxItem)
|
||||
.filter(FinishedGoodsBoxItem.id == box_item_id)
|
||||
@@ -267,13 +295,32 @@ def update_box_item(db: Session, box_item_id: int, req: BoxUpdateRequest) -> dic
|
||||
.first()
|
||||
)
|
||||
|
||||
if target_box is not None and target_box.id != current_box.id:
|
||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||
|
||||
if target_box is None:
|
||||
target_box = FinishedGoodsBox(paichan_no=paichan_no, box_no=req.box_no)
|
||||
db.add(target_box)
|
||||
db.flush()
|
||||
else:
|
||||
duplicate_item = (
|
||||
db.query(FinishedGoodsBoxItem)
|
||||
.filter(
|
||||
FinishedGoodsBoxItem.box_id == target_box.id,
|
||||
FinishedGoodsBoxItem.zongpai_no == item.zongpai_no,
|
||||
FinishedGoodsBoxItem.id != item.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if duplicate_item is not None:
|
||||
raise DuplicateBoxItemError(paichan_no, req.box_no)
|
||||
|
||||
erp = query_erp_info(db, item.zongpai_no)
|
||||
_ensure_quantity_within_erp_total(
|
||||
db,
|
||||
paichan_no,
|
||||
item.zongpai_no,
|
||||
erp["quantity"],
|
||||
req.quantity,
|
||||
exclude_box_item_id=box_item_id,
|
||||
)
|
||||
|
||||
old_box_id = item.box_id
|
||||
item.box_id = target_box.id
|
||||
|
||||
Reference in New Issue
Block a user