| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- """库位节点编号路径与储物格完整编号计算、快照级联刷新。"""
- 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)
|