import re from sqlalchemy import bindparam, text from sqlalchemy.orm import Session from app.models.attachment import ( AttachmentCategory, FinishedGoodsAttachment, FinishedGoodsAttachmentItem, FinishedGoodsBoxAttachmentItem, ) from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem from app.schemas.box import BoxSaveRequest, BoxUpdateRequest ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$") class InvalidZongpaiError(Exception): pass class ZongpaiNotFoundError(Exception): pass class DuplicateBoxItemError(Exception): def __init__(self, paichan_no: str, box_no: int): self.paichan_no = paichan_no self.box_no = box_no class BoxItemNotFoundError(Exception): pass 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)) row = db.execute(sql, {"zongpai_no": zongpai_no}).fetchone() if not row: raise ZongpaiNotFoundError() return { "zongpai_no": row[0], "paichan_no": row[1], "quantity": int(row[2]), "work_order_no": row[3], } def _erp_info_sql(db: Session) -> str: dialect_name = db.bind.dialect.name if db.bind is not None else "" if dialect_name == "postgresql": return ( 'SELECT "总排号", "排产号", "数量", "工令号" ' 'FROM "ERPAuto"."vw_productionContractData" ' 'WHERE "总排号" = :zongpai_no ' "LIMIT 1" ) return ( "SELECT TOP 1 [总排号], [排产号], [数量], [工令号] " "FROM [ERPAuto].[vw_productionContractData] " "WHERE [总排号] = :zongpai_no" ) def query_erp_item_info_map(db: Session, zongpai_nos: list[str]) -> dict[str, dict]: """批量查询总排号对应的工令号和 ERP 总数量。""" if not zongpai_nos: return {} dialect_name = db.bind.dialect.name if db.bind is not None else "" if dialect_name == "postgresql": sql = text( 'SELECT "总排号", "工令号", "数量" ' 'FROM "ERPAuto"."vw_productionContractData" ' 'WHERE "总排号" IN :zongpai_nos' ) else: sql = text( "SELECT [总排号], [工令号], [数量] " "FROM [ERPAuto].[vw_productionContractData] " "WHERE [总排号] IN :zongpai_nos" ) rows = db.execute( sql.bindparams(bindparam("zongpai_nos", expanding=True)), {"zongpai_nos": sorted(set(zongpai_nos))}, ).fetchall() return { row[0]: { "work_order_no": row[1], "total_quantity": int(row[2]) if row[2] is not None else None, } for row in rows } def get_box_info(db: Session, zongpai_no: str) -> dict: """获取装箱信息:排产号、数量、已有箱号明细。""" zongpai_no = zongpai_no.strip().upper() if not ZONGPAI_PATTERN.match(zongpai_no): raise InvalidZongpaiError() erp = query_erp_info(db, zongpai_no) paichan_no = erp["paichan_no"] quantity = erp["quantity"] boxes = ( db.query(FinishedGoodsBox) .filter(FinishedGoodsBox.paichan_no == paichan_no) .order_by(FinishedGoodsBox.box_no) .all() ) box_items = {} item_zongpai_nos = [] for box in boxes: items = ( db.query(FinishedGoodsBoxItem) .filter(FinishedGoodsBoxItem.box_id == box.id) .all() ) box_items[box.id] = items item_zongpai_nos.extend(item.zongpai_no for item in items) erp_item_info_map = query_erp_item_info_map(db, item_zongpai_nos) existing_boxes = [] current_zongpai_boxes = [] max_box_no = 0 paichan_box_rows = ( db.query(FinishedGoodsBox) .filter(FinishedGoodsBox.paichan_no == paichan_no) .all() ) att_rows = ( db.query(FinishedGoodsBoxAttachmentItem) .filter( FinishedGoodsBoxAttachmentItem.box_id.in_( [b.id for b in paichan_box_rows] ) ) .all() ) att_category_ids = {r.category_id for r in att_rows} att_names = { c.id: c.name for c in db.query(AttachmentCategory) .filter(AttachmentCategory.id.in_(list(att_category_ids))) .all() } if att_category_ids else {} items_by_box: dict[int, list[dict]] = {} for r in att_rows: items_by_box.setdefault(r.box_id, []).append( { "box_item_id": r.id, "zongpai_no": r.zongpai_no, "work_order_no": None, "quantity": int(r.quantity or 0), "total_quantity": None, "kind": "attachment", "category_id": r.category_id, "category_name": att_names.get(r.category_id), } ) for box in boxes: 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"), "kind": "product", } for item in items ] + items_by_box.get(box.id, []), }) if box.box_no > max_box_no: max_box_no = box.box_no # Attachment summary for the scanned总排号 att_row = ( db.query(FinishedGoodsAttachment) .filter(FinishedGoodsAttachment.zongpai_no == zongpai_no) .first() ) determination = att_row.determination if att_row else "undetermined" plan_items = ( db.query(FinishedGoodsAttachmentItem) .filter(FinishedGoodsAttachmentItem.zongpai_no == zongpai_no) .all() ) pending_count = 0 for it in plan_items: boxed = sum( int(r.quantity or 0) for r in db.query(FinishedGoodsBoxAttachmentItem) .filter( FinishedGoodsBoxAttachmentItem.zongpai_no == zongpai_no, FinishedGoodsBoxAttachmentItem.category_id == it.category_id, ) .all() ) if boxed < it.expected_qty: pending_count += 1 all_complete = ( determination == "has" and len(plan_items) > 0 and pending_count == 0 ) attachment_summary = { "determination": determination, "all_complete": all_complete, "pending_count": pending_count, } return { "zongpai_no": zongpai_no, "paichan_no": paichan_no, "work_order_no": erp["work_order_no"], "quantity": quantity, "current_zongpai_boxes": current_zongpai_boxes, "existing_boxes": existing_boxes, "max_box_no": max_box_no, "suggested_box_no": max_box_no + 1, "attachment_summary": attachment_summary, } 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"] box = ( db.query(FinishedGoodsBox) .filter( FinishedGoodsBox.paichan_no == paichan_no, FinishedGoodsBox.box_no == req.box_no, ) .first() ) if box: existing_item = ( db.query(FinishedGoodsBoxItem) .filter( FinishedGoodsBoxItem.box_id == box.id, FinishedGoodsBoxItem.zongpai_no == req.zongpai_no, ) .first() ) 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 box is None: box = FinishedGoodsBox( paichan_no=paichan_no, box_no=req.box_no, ) db.add(box) db.flush() item = FinishedGoodsBoxItem( box_id=box.id, zongpai_no=req.zongpai_no, quantity=req.quantity, ) db.add(item) db.commit() db.refresh(item) return { "box_item_id": item.id, "paichan_no": paichan_no, "box_no": req.box_no, "zongpai_no": req.zongpai_no, "quantity": req.quantity, "created_at": item.created_at.isoformat(), } def update_box_item(db: Session, box_item_id: int, req: BoxUpdateRequest) -> dict: """更新已分配明细的箱号和数量。""" item = ( db.query(FinishedGoodsBoxItem) .filter(FinishedGoodsBoxItem.id == box_item_id) .first() ) if item is None: raise BoxItemNotFoundError() current_box = ( db.query(FinishedGoodsBox) .filter(FinishedGoodsBox.id == item.box_id) .first() ) if current_box is None: raise InvalidBoxItemError() paichan_no = current_box.paichan_no target_box = ( db.query(FinishedGoodsBox) .filter( FinishedGoodsBox.paichan_no == paichan_no, FinishedGoodsBox.box_no == req.box_no, ) .first() ) 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 item.quantity = req.quantity if old_box_id != target_box.id: old_box_has_items = ( db.query(FinishedGoodsBoxItem.id) .filter( FinishedGoodsBoxItem.box_id == old_box_id, FinishedGoodsBoxItem.id != item.id, ) .first() ) if old_box_has_items is None: old_box = ( db.query(FinishedGoodsBox) .filter(FinishedGoodsBox.id == old_box_id) .first() ) if old_box is not None: db.delete(old_box) db.commit() db.refresh(item) return { "box_item_id": item.id, "paichan_no": paichan_no, "box_no": req.box_no, "zongpai_no": item.zongpai_no, "quantity": int(item.quantity or 0), "updated_at": item.created_at.isoformat(), } def delete_box_item(db: Session, box_item_id: int) -> dict: """删除误录的装箱明细;若箱号下无其他明细,同步删除空箱号。""" item = ( db.query(FinishedGoodsBoxItem) .filter(FinishedGoodsBoxItem.id == box_item_id) .first() ) if item is None: raise BoxItemNotFoundError() box_id = item.box_id db.delete(item) db.flush() remaining = ( db.query(FinishedGoodsBoxItem.id) .filter(FinishedGoodsBoxItem.box_id == box_id) .first() ) if remaining is None: box = db.query(FinishedGoodsBox).filter(FinishedGoodsBox.id == box_id).first() if box is not None: db.delete(box) db.commit() return {"box_item_id": box_item_id, "deleted": True}