落库并展示质检工单级结论(可编辑/任务派生),库位树支持批量新增子节点与货架格子检索,协议/捐赠列表补充接收与门禁质检摘要,并同步生产物料与工单卡片展示优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
273 lines
7.8 KiB
Python
273 lines
7.8 KiB
Python
"""库位节点编号路径与储物格完整编号计算、快照级联刷新。"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Union
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models.models import (
|
||
BizCellProduct,
|
||
BizOutboundReleaseCell,
|
||
BizStorageFacility,
|
||
BizStorageFacilitySlot,
|
||
)
|
||
|
||
SLOT_LAYOUT_ROW_STRIDE = 1000
|
||
|
||
|
||
def encode_slot_layout_order(row: int, col: int) -> int:
|
||
"""将货架行列位置编码为 order_num(row*1000+col)。"""
|
||
row_i = int(row)
|
||
col_i = int(col)
|
||
if row_i < 1 or col_i < 1:
|
||
raise ValueError("行列序号必须从 1 开始")
|
||
if col_i >= SLOT_LAYOUT_ROW_STRIDE:
|
||
raise ValueError(f"列数不能超过 {SLOT_LAYOUT_ROW_STRIDE - 1}")
|
||
return row_i * SLOT_LAYOUT_ROW_STRIDE + col_i
|
||
|
||
|
||
def decode_slot_layout_order(order_num: int) -> tuple[int, int]:
|
||
"""解析 order_num 中的行列位置;非布局编码时返回 (0, order)。"""
|
||
order = int(order_num or 0)
|
||
if order >= SLOT_LAYOUT_ROW_STRIDE:
|
||
return order // SLOT_LAYOUT_ROW_STRIDE, order % SLOT_LAYOUT_ROW_STRIDE
|
||
return 0, order
|
||
|
||
|
||
def validate_node_code(node_code: str) -> str:
|
||
code = (node_code or "").strip()
|
||
if not code:
|
||
return ""
|
||
if len(code) > 32:
|
||
raise ValueError("节点编号长度不能超过 32 个字符")
|
||
return code
|
||
|
||
|
||
def assert_sibling_node_code_unique(
|
||
db: Session,
|
||
*,
|
||
parent_id: str,
|
||
node_code: str,
|
||
tenant_no: str,
|
||
exclude_id: Optional[str] = None,
|
||
) -> None:
|
||
pid = (parent_id or "0").strip() or "0"
|
||
code = validate_node_code(node_code)
|
||
if not code:
|
||
return
|
||
q = db.query(BizStorageFacility).filter(
|
||
BizStorageFacility.parent_id == pid,
|
||
BizStorageFacility.node_code == code,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
BizStorageFacility.del_flag == 0,
|
||
)
|
||
if exclude_id:
|
||
q = q.filter(BizStorageFacility.id != exclude_id)
|
||
if q.first():
|
||
raise ValueError("同级节点编号已存在")
|
||
|
||
|
||
def build_node_code_path(
|
||
node_id: str,
|
||
facility_by_id: Dict[str, Union[BizStorageFacility, dict]],
|
||
) -> str:
|
||
parts: List[str] = []
|
||
cur_id = (node_id or "").strip()
|
||
visited: set[str] = set()
|
||
while cur_id and cur_id != "0" and cur_id not in visited:
|
||
visited.add(cur_id)
|
||
node = facility_by_id.get(cur_id)
|
||
if not node:
|
||
break
|
||
if isinstance(node, dict):
|
||
code = (node.get("nodeCode") or node.get("node_code") or "").strip()
|
||
parent_id = (node.get("parentId") or node.get("parent_id") or "0").strip()
|
||
else:
|
||
code = (node.node_code or "").strip()
|
||
parent_id = (node.parent_id or "0").strip()
|
||
if code:
|
||
parts.append(code)
|
||
cur_id = parent_id
|
||
parts.reverse()
|
||
return "-".join(parts)
|
||
|
||
|
||
def build_full_slot_code(
|
||
facility_node_id: str,
|
||
slot_code: Optional[str],
|
||
facility_by_id: Dict[str, Union[BizStorageFacility, dict]],
|
||
) -> str:
|
||
path = build_node_code_path(facility_node_id, facility_by_id)
|
||
local = (slot_code or "").strip()
|
||
if path and local:
|
||
return f"{path}-{local}"
|
||
return local or path
|
||
|
||
|
||
def load_facility_map(db: Session, tenant_no: str) -> Dict[str, BizStorageFacility]:
|
||
rows = (
|
||
db.query(BizStorageFacility)
|
||
.filter(
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
BizStorageFacility.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
return {r.id: r for r in rows}
|
||
|
||
|
||
def load_facility_ancestor_map(
|
||
db: Session,
|
||
tenant_no: str,
|
||
node_id: str,
|
||
) -> Dict[str, BizStorageFacility]:
|
||
"""仅加载某节点到根的祖先链(含自身),供单节点格子列表拼 fullSlotCode。"""
|
||
result: Dict[str, BizStorageFacility] = {}
|
||
cur_id = (node_id or "").strip()
|
||
while cur_id and cur_id != "0" and cur_id not in result:
|
||
row = (
|
||
db.query(BizStorageFacility)
|
||
.filter(
|
||
BizStorageFacility.id == cur_id,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
BizStorageFacility.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not row:
|
||
break
|
||
result[row.id] = row
|
||
cur_id = (row.parent_id or "0").strip()
|
||
return result
|
||
|
||
|
||
def collect_slot_ids_under_nodes(
|
||
db: Session,
|
||
node_ids: List[str],
|
||
tenant_no: str,
|
||
*,
|
||
collect_descendant_ids_fn,
|
||
) -> List[str]:
|
||
all_node_ids: set[str] = set()
|
||
for nid in node_ids:
|
||
nid = (nid or "").strip()
|
||
if not nid:
|
||
continue
|
||
all_node_ids.add(nid)
|
||
all_node_ids.update(collect_descendant_ids_fn(db, nid, tenant_no))
|
||
if not all_node_ids:
|
||
return []
|
||
|
||
leaves = (
|
||
db.query(BizStorageFacility)
|
||
.filter(
|
||
BizStorageFacility.id.in_(all_node_ids),
|
||
BizStorageFacility.is_leaf == 1,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
BizStorageFacility.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
leaf_ids = [leaf.id for leaf in leaves]
|
||
if not leaf_ids:
|
||
return []
|
||
|
||
slots = (
|
||
db.query(BizStorageFacilitySlot)
|
||
.filter(
|
||
BizStorageFacilitySlot.facility_node_id.in_(leaf_ids),
|
||
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
return [s.id for s in slots]
|
||
|
||
|
||
def refresh_outbound_release_cell_slot_names(
|
||
db: Session,
|
||
slot_full_map: Dict[str, str],
|
||
tenant_no: str,
|
||
) -> int:
|
||
if not slot_full_map:
|
||
return 0
|
||
|
||
products = (
|
||
db.query(BizCellProduct)
|
||
.filter(
|
||
BizCellProduct.storage_slot_id.in_(list(slot_full_map.keys())),
|
||
BizCellProduct.tenant_no == tenant_no,
|
||
BizCellProduct.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
product_name_map = {
|
||
p.id: slot_full_map.get((p.storage_slot_id or "").strip(), "")
|
||
for p in products
|
||
if (p.storage_slot_id or "").strip()
|
||
}
|
||
if not product_name_map:
|
||
return 0
|
||
|
||
cells = (
|
||
db.query(BizOutboundReleaseCell)
|
||
.filter(
|
||
BizOutboundReleaseCell.cell_product_id.in_(list(product_name_map.keys())),
|
||
BizOutboundReleaseCell.tenant_no == tenant_no,
|
||
BizOutboundReleaseCell.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
now = datetime.now()
|
||
updated = 0
|
||
for cell in cells:
|
||
new_name = product_name_map.get(cell.cell_product_id, "")
|
||
if new_name and cell.storage_slot_name != new_name:
|
||
cell.storage_slot_name = new_name
|
||
cell.update_time = now
|
||
updated += 1
|
||
return updated
|
||
|
||
|
||
def cascade_refresh_slot_name_snapshots_for_slots(
|
||
db: Session,
|
||
slot_ids: List[str],
|
||
tenant_no: str,
|
||
) -> int:
|
||
ids = [(sid or "").strip() for sid in slot_ids if (sid or "").strip()]
|
||
if not ids:
|
||
return 0
|
||
|
||
slots = (
|
||
db.query(BizStorageFacilitySlot)
|
||
.filter(
|
||
BizStorageFacilitySlot.id.in_(ids),
|
||
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
if not slots:
|
||
return 0
|
||
|
||
facility_by_id = load_facility_map(db, tenant_no)
|
||
slot_full_map = {
|
||
s.id: build_full_slot_code(s.facility_node_id, s.slot_code, facility_by_id)
|
||
for s in slots
|
||
}
|
||
return refresh_outbound_release_cell_slot_names(db, slot_full_map, tenant_no)
|
||
|
||
|
||
def cascade_refresh_slot_name_snapshots_for_nodes(
|
||
db: Session,
|
||
node_ids: List[str],
|
||
tenant_no: str,
|
||
*,
|
||
collect_descendant_ids_fn,
|
||
) -> int:
|
||
slot_ids = collect_slot_ids_under_nodes(
|
||
db, node_ids, tenant_no, collect_descendant_ids_fn=collect_descendant_ids_fn
|
||
)
|
||
return cascade_refresh_slot_name_snapshots_for_slots(db, slot_ids, tenant_no)
|