feat(server): add POST /attachment/box with real-evidence flip + cap
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.models.attachment import AttachmentCategory
|
||||
from app.schemas.attachment import (
|
||||
AttachmentBoxSaveRequest,
|
||||
AttachmentBoxSaveResponse,
|
||||
AttachmentConfigRequest,
|
||||
AttachmentLocationRequest,
|
||||
AttachmentLocationResponse,
|
||||
@@ -15,6 +17,7 @@ from app.services.attachment_service import (
|
||||
configure_attachment,
|
||||
get_attachment_status,
|
||||
register_attachment_location,
|
||||
save_attachment_box,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["attachment"])
|
||||
@@ -64,3 +67,18 @@ def post_attachment_location(
|
||||
created_at=record.created_at.isoformat(),
|
||||
previous_location=getattr(record, "previous_location", None),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/attachment/box",
|
||||
response_model=AttachmentBoxSaveResponse,
|
||||
responses={
|
||||
400: {"description": "参数非法或超量", "model": ErrorResponse},
|
||||
404: {"description": "总排号/类型不存在", "model": ErrorResponse},
|
||||
409: {"description": "同箱同类型重复", "model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def post_attachment_box(
|
||||
req: AttachmentBoxSaveRequest, db: Session = Depends(get_db)
|
||||
):
|
||||
return save_attachment_box(db, req)
|
||||
|
||||
16
app/main.py
16
app/main.py
@@ -36,6 +36,7 @@ from app.services.attachment_service import (
|
||||
AlreadyOffShelfAttachmentError,
|
||||
CategoryHasBoxesError,
|
||||
CategoryNotFoundError,
|
||||
DuplicateAttachmentBoxItemError,
|
||||
DuplicateAttachmentLocationError,
|
||||
)
|
||||
|
||||
@@ -231,6 +232,21 @@ async def already_off_shelf_attachment_handler(
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(DuplicateAttachmentBoxItemError)
|
||||
async def duplicate_attachment_box_handler(
|
||||
request: Request, exc: DuplicateAttachmentBoxItemError
|
||||
):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={
|
||||
"error_code": "DUPLICATE_BOX_NO",
|
||||
"message": f"排产号 {exc.paichan_no} 下箱号 {exc.box_no} 已存在",
|
||||
"paichan_no": exc.paichan_no,
|
||||
"box_no": exc.box_no,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to CargoTrace API"}
|
||||
|
||||
@@ -11,8 +11,13 @@ from app.models.attachment import (
|
||||
FinishedGoodsAttachmentLocation,
|
||||
FinishedGoodsBoxAttachmentItem,
|
||||
)
|
||||
from app.models.finished_goods import FinishedGoodsBox
|
||||
from app.schemas.attachment import AttachmentConfigRequest
|
||||
from app.services.box_service import InvalidZongpaiError
|
||||
from app.services.box_service import (
|
||||
InvalidQuantityError,
|
||||
InvalidZongpaiError,
|
||||
query_erp_info,
|
||||
)
|
||||
|
||||
ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$")
|
||||
|
||||
@@ -257,3 +262,112 @@ def register_attachment_location(db: Session, req) -> FinishedGoodsAttachmentLoc
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
return record
|
||||
|
||||
|
||||
def _boxed_for_category(
|
||||
db: Session, zongpai_no: str, category_id: int, exclude_item_id: int | None = None
|
||||
) -> int:
|
||||
q = db.query(FinishedGoodsBoxAttachmentItem).filter(
|
||||
FinishedGoodsBoxAttachmentItem.zongpai_no == zongpai_no,
|
||||
FinishedGoodsBoxAttachmentItem.category_id == category_id,
|
||||
)
|
||||
if exclude_item_id is not None:
|
||||
q = q.filter(FinishedGoodsBoxAttachmentItem.id != exclude_item_id)
|
||||
return sum(int(r.quantity or 0) for r in q.all())
|
||||
|
||||
|
||||
def _ensure_attachment_item(db: Session, zongpai_no: str, category_id: int, erp_qty: int):
|
||||
"""实物证据:不存在则建 item(expected 兜底 = ERP 数量),并保证 determination=has。"""
|
||||
if not _category_name_map(db, [category_id]):
|
||||
raise CategoryNotFoundError(category_id)
|
||||
|
||||
att = (
|
||||
db.query(FinishedGoodsAttachment)
|
||||
.filter(FinishedGoodsAttachment.zongpai_no == zongpai_no)
|
||||
.first()
|
||||
)
|
||||
if att is None:
|
||||
att = FinishedGoodsAttachment(zongpai_no=zongpai_no, determination="has")
|
||||
db.add(att)
|
||||
elif att.determination in ("undetermined", "none"):
|
||||
att.determination = "has"
|
||||
att.updated_at = datetime.now()
|
||||
db.flush()
|
||||
|
||||
item = (
|
||||
db.query(FinishedGoodsAttachmentItem)
|
||||
.filter(
|
||||
FinishedGoodsAttachmentItem.zongpai_no == zongpai_no,
|
||||
FinishedGoodsAttachmentItem.category_id == category_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if item is None:
|
||||
item = FinishedGoodsAttachmentItem(
|
||||
zongpai_no=zongpai_no,
|
||||
category_id=category_id,
|
||||
expected_qty=erp_qty,
|
||||
)
|
||||
db.add(item)
|
||||
db.flush()
|
||||
return item
|
||||
|
||||
|
||||
def save_attachment_box(db: Session, req) -> dict:
|
||||
zongpai_no = _validate_zongpai(req.zongpai_no)
|
||||
|
||||
erp = query_erp_info(db, zongpai_no) # raises ZongpaiNotFoundError if missing
|
||||
paichan_no = erp["paichan_no"]
|
||||
erp_qty = erp["quantity"]
|
||||
|
||||
item = _ensure_attachment_item(db, zongpai_no, req.category_id, erp_qty)
|
||||
|
||||
box = (
|
||||
db.query(FinishedGoodsBox)
|
||||
.filter(
|
||||
FinishedGoodsBox.paichan_no == paichan_no,
|
||||
FinishedGoodsBox.box_no == req.box_no,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if box is not None:
|
||||
existing_in_box = (
|
||||
db.query(FinishedGoodsBoxAttachmentItem)
|
||||
.filter(
|
||||
FinishedGoodsBoxAttachmentItem.box_id == box.id,
|
||||
FinishedGoodsBoxAttachmentItem.zongpai_no == zongpai_no,
|
||||
FinishedGoodsBoxAttachmentItem.category_id == req.category_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing_in_box is not None:
|
||||
raise DuplicateAttachmentBoxItemError(paichan_no, req.box_no)
|
||||
else:
|
||||
box = FinishedGoodsBox(paichan_no=paichan_no, box_no=req.box_no)
|
||||
db.add(box)
|
||||
db.flush()
|
||||
|
||||
# Cap check against expected_qty (mirrors product _ensure_quantity_within_erp_total)
|
||||
already_boxed = _boxed_for_category(db, zongpai_no, req.category_id)
|
||||
if already_boxed + req.quantity > item.expected_qty:
|
||||
raise InvalidQuantityError()
|
||||
|
||||
record = FinishedGoodsBoxAttachmentItem(
|
||||
box_id=box.id,
|
||||
zongpai_no=zongpai_no,
|
||||
category_id=req.category_id,
|
||||
quantity=req.quantity,
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
return {
|
||||
"box_item_id": record.id,
|
||||
"paichan_no": paichan_no,
|
||||
"box_no": req.box_no,
|
||||
"zongpai_no": zongpai_no,
|
||||
"category_id": req.category_id,
|
||||
"quantity": req.quantity,
|
||||
"created_at": record.created_at.isoformat(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user