锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
1280 lines
40 KiB
Python
1280 lines
40 KiB
Python
"""存储设施(树结构)+ 小格 CRUD(biz_storage_facility / biz_storage_facility_slot)"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
import re
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy import and_, case, false, func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid
|
||
from app.models.models import BizStorageFacility, BizStorageFacilitySlot
|
||
from app.services.storage_facility_code import (
|
||
assert_sibling_node_code_unique,
|
||
build_full_slot_code,
|
||
build_node_code_path,
|
||
cascade_refresh_slot_name_snapshots_for_nodes,
|
||
cascade_refresh_slot_name_snapshots_for_slots,
|
||
decode_slot_layout_order,
|
||
encode_slot_layout_order,
|
||
load_facility_ancestor_map,
|
||
load_facility_map,
|
||
validate_node_code,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Facility node helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _row_to_dict(f: BizStorageFacility) -> Dict[str, Any]:
|
||
return {
|
||
"id": f.id,
|
||
"parentId": f.parent_id,
|
||
"nodeName": f.node_name,
|
||
"nodeCode": f.node_code or "",
|
||
"nodeType": f.node_type or "",
|
||
"isLeaf": 1 if f.is_leaf else 0,
|
||
"tenantNo": f.tenant_no,
|
||
"orderNum": f.order_num if f.order_num is not None else 0,
|
||
"remark": f.remark or "",
|
||
"createTime": f.create_time.strftime("%Y-%m-%d %H:%M:%S") if f.create_time else "",
|
||
"updateTime": f.update_time.strftime("%Y-%m-%d %H:%M:%S") if f.update_time else "",
|
||
}
|
||
|
||
|
||
def list_all_facilities(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
keyword: Optional[str] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Return FLAT list of all non-deleted facility nodes (no pagination, no tree building)."""
|
||
q = db.query(BizStorageFacility).filter(BizStorageFacility.del_flag == 0)
|
||
if tenant_no:
|
||
q = q.filter(BizStorageFacility.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q = q.filter(
|
||
or_(
|
||
BizStorageFacility.node_name.like(like),
|
||
BizStorageFacility.node_code.like(like),
|
||
)
|
||
)
|
||
rows = q.order_by(BizStorageFacility.order_num.asc(), BizStorageFacility.create_time.asc()).all()
|
||
return [_row_to_dict(x) for x in rows]
|
||
|
||
|
||
def get_facility(db: Session, node_id: str, tenant_no: Optional[str] = None) -> Optional[BizStorageFacility]:
|
||
q = db.query(BizStorageFacility).filter(
|
||
and_(BizStorageFacility.id == node_id, BizStorageFacility.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizStorageFacility.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def get_children(db: Session, parent_id: str, tenant_no: str) -> List[BizStorageFacility]:
|
||
"""Direct children of a node."""
|
||
return (
|
||
db.query(BizStorageFacility)
|
||
.filter(
|
||
and_(
|
||
BizStorageFacility.parent_id == parent_id,
|
||
BizStorageFacility.del_flag == 0,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
)
|
||
)
|
||
.order_by(BizStorageFacility.order_num.asc(), BizStorageFacility.create_time.asc())
|
||
.all()
|
||
)
|
||
|
||
|
||
def collect_descendant_ids(db: Session, node_id: str, tenant_no: str) -> List[str]:
|
||
"""Iteratively collect ALL descendant node IDs (stack-based, no recursion)."""
|
||
ids: List[str] = []
|
||
stack: List[str] = [node_id]
|
||
while stack:
|
||
current = stack.pop()
|
||
children = get_children(db, current, tenant_no)
|
||
for child in children:
|
||
ids.append(child.id)
|
||
stack.append(child.id)
|
||
return ids
|
||
|
||
|
||
def create_facility(
|
||
db: Session,
|
||
*,
|
||
parent_id: str = "0",
|
||
node_name: str,
|
||
node_code: str,
|
||
node_type: Optional[str] = None,
|
||
is_leaf: int = 0,
|
||
tenant_no: str = "zkxw",
|
||
order_num: Optional[int] = None,
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> BizStorageFacility:
|
||
name = (node_name or "").strip()
|
||
if not name:
|
||
raise ValueError("节点名称不能为空")
|
||
code = validate_node_code(node_code)
|
||
pid = parent_id or "0"
|
||
assert_sibling_node_code_unique(
|
||
db, parent_id=pid, node_code=code, tenant_no=tenant_no
|
||
)
|
||
obj = BizStorageFacility(
|
||
id=generate_uuid(),
|
||
parent_id=pid,
|
||
node_name=name,
|
||
node_code=code,
|
||
node_type=(node_type or "").strip() or None,
|
||
is_leaf=is_leaf,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num if order_num is not None else 0,
|
||
remark=(remark or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def _format_seq(num: int, pad_width: int) -> str:
|
||
s = str(num)
|
||
width = int(pad_width or 0)
|
||
if width <= 0:
|
||
return s
|
||
return s.zfill(width)
|
||
|
||
|
||
def batch_create_facilities(
|
||
db: Session,
|
||
*,
|
||
parent_id: str = "0",
|
||
name_prefix: str = "",
|
||
name_suffix: str = "",
|
||
start_num: int,
|
||
end_num: int,
|
||
pad_width: int = 0,
|
||
code_prefix: str = "",
|
||
code_suffix: str = "",
|
||
node_type: Optional[str] = None,
|
||
is_leaf: int = 0,
|
||
tenant_no: str = "zkxw",
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""按「前缀 + 序号 + 后缀」批量创建同级子节点。"""
|
||
if start_num < 0 or end_num < 0:
|
||
raise ValueError("序号不能为负数")
|
||
if end_num < start_num:
|
||
raise ValueError("结束序号不能小于起始序号")
|
||
count = end_num - start_num + 1
|
||
if count > 200:
|
||
raise ValueError("单次最多创建 200 个子节点")
|
||
if pad_width < 0 or pad_width > 8:
|
||
raise ValueError("位数补零须在 0~8 之间")
|
||
|
||
pid = (parent_id or "0").strip() or "0"
|
||
if pid != "0":
|
||
parent = get_facility(db, pid, tenant_no=tenant_no)
|
||
if not parent:
|
||
raise ValueError("上级节点不存在")
|
||
|
||
name_prefix = name_prefix or ""
|
||
name_suffix = name_suffix or ""
|
||
code_prefix = code_prefix or ""
|
||
code_suffix = code_suffix or ""
|
||
node_type_val = (node_type or "").strip() or None
|
||
remark_val = (remark or "").strip() or None
|
||
leaf_val = 1 if int(is_leaf or 0) == 1 else 0
|
||
now = datetime.now()
|
||
|
||
planned: List[tuple[str, str, int]] = []
|
||
seen_codes: set[str] = set()
|
||
for num in range(start_num, end_num + 1):
|
||
seq = _format_seq(num, pad_width)
|
||
name = f"{name_prefix}{seq}{name_suffix}".strip()
|
||
if not name:
|
||
raise ValueError("节点名称不能为空,请填写前缀或后缀")
|
||
# 前缀、后缀都空时,节点编号 = 序号(含补零)
|
||
code = validate_node_code(f"{code_prefix}{seq}{code_suffix}")
|
||
if not code:
|
||
raise ValueError("节点编号不能为空")
|
||
if code in seen_codes:
|
||
raise ValueError(f"批次内节点编号重复:{code}")
|
||
seen_codes.add(code)
|
||
assert_sibling_node_code_unique(
|
||
db, parent_id=pid, node_code=code, tenant_no=tenant_no
|
||
)
|
||
planned.append((name, code, num))
|
||
|
||
objs: List[BizStorageFacility] = []
|
||
for name, code, num in planned:
|
||
obj = BizStorageFacility(
|
||
id=generate_uuid(),
|
||
parent_id=pid,
|
||
node_name=name,
|
||
node_code=code,
|
||
node_type=node_type_val,
|
||
is_leaf=leaf_val,
|
||
tenant_no=tenant_no,
|
||
order_num=num,
|
||
remark=remark_val,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
objs.append(obj)
|
||
db.commit()
|
||
for obj in objs:
|
||
db.refresh(obj)
|
||
return [_row_to_dict(obj) for obj in objs]
|
||
|
||
|
||
def update_facility(
|
||
db: Session,
|
||
node_id: str,
|
||
*,
|
||
node_name: Optional[str] = None,
|
||
node_code: Optional[str] = None,
|
||
node_type: Optional[str] = None,
|
||
is_leaf: Optional[int] = None,
|
||
order_num: Optional[int] = None,
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
tenant_no: Optional[str] = None,
|
||
) -> BizStorageFacility:
|
||
obj = get_facility(db, node_id, tenant_no=tenant_no)
|
||
if not obj:
|
||
raise ValueError("存储设施节点不存在")
|
||
old_node_code = (obj.node_code or "").strip()
|
||
if node_name is not None:
|
||
name = (node_name or "").strip()
|
||
if not name:
|
||
raise ValueError("节点名称不能为空")
|
||
obj.node_name = name
|
||
if node_code is not None:
|
||
code = validate_node_code(node_code)
|
||
assert_sibling_node_code_unique(
|
||
db,
|
||
parent_id=obj.parent_id or "0",
|
||
node_code=code,
|
||
tenant_no=obj.tenant_no,
|
||
exclude_id=obj.id,
|
||
)
|
||
obj.node_code = code
|
||
if node_type is not None:
|
||
obj.node_type = (node_type or "").strip() or None
|
||
if is_leaf is not None:
|
||
obj.is_leaf = is_leaf
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = (remark or "").strip() or None
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
|
||
node_code_changed = node_code is not None and (obj.node_code or "").strip() != old_node_code
|
||
if node_code_changed:
|
||
cascade_refresh_slot_name_snapshots_for_nodes(
|
||
db,
|
||
[node_id],
|
||
obj.tenant_no,
|
||
collect_descendant_ids_fn=collect_descendant_ids,
|
||
)
|
||
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_facility(db: Session, node_id: str, tenant_no: Optional[str] = None) -> None:
|
||
"""Cascade soft-delete: node + all descendants + their slots."""
|
||
node = get_facility(db, node_id, tenant_no=tenant_no)
|
||
if not node:
|
||
raise ValueError("存储设施节点不存在")
|
||
# Collect all descendant IDs
|
||
all_ids = [node_id] + collect_descendant_ids(db, node_id, node.tenant_no)
|
||
now = datetime.now()
|
||
# Soft-delete all slots belonging to these nodes
|
||
db.query(BizStorageFacilitySlot).filter(
|
||
BizStorageFacilitySlot.facility_node_id.in_(all_ids),
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
||
# Soft-delete all facility nodes
|
||
db.query(BizStorageFacility).filter(
|
||
BizStorageFacility.id.in_(all_ids),
|
||
BizStorageFacility.del_flag == 0,
|
||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
||
db.commit()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Slot helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _slot_to_dict(
|
||
s: BizStorageFacilitySlot,
|
||
facility_by_id: Optional[Dict[str, BizStorageFacility]] = None,
|
||
) -> Dict[str, Any]:
|
||
occupied_count = s.occupied_count if s.occupied_count is not None else 0
|
||
full_slot_code = ""
|
||
if facility_by_id is not None:
|
||
full_slot_code = build_full_slot_code(s.facility_node_id, s.slot_code, facility_by_id)
|
||
return {
|
||
"id": s.id,
|
||
"facilityNodeId": s.facility_node_id,
|
||
"slotCode": s.slot_code or "",
|
||
"fullSlotCode": full_slot_code,
|
||
"slotStatus": s.slot_status or "",
|
||
"occupiedCount": int(occupied_count),
|
||
"tenantNo": s.tenant_no,
|
||
"orderNum": s.order_num if s.order_num is not None else 0,
|
||
"remark": s.remark or "",
|
||
"createTime": s.create_time.strftime("%Y-%m-%d %H:%M:%S") if s.create_time else "",
|
||
"updateTime": s.update_time.strftime("%Y-%m-%d %H:%M:%S") if s.update_time else "",
|
||
}
|
||
|
||
|
||
def aggregate_slot_stats_by_facility(*, query) -> List[Dict[str, Any]]:
|
||
"""按设施叶子节点聚合格子总数与占用数(单次 GROUP BY,供树节点徽章展示)。"""
|
||
rows = (
|
||
query.with_entities(
|
||
BizStorageFacilitySlot.facility_node_id,
|
||
func.count(BizStorageFacilitySlot.id).label("total"),
|
||
func.sum(
|
||
case((BizStorageFacilitySlot.slot_status == "占用", 1), else_=0)
|
||
).label("occupied"),
|
||
)
|
||
.group_by(BizStorageFacilitySlot.facility_node_id)
|
||
.all()
|
||
)
|
||
result: List[Dict[str, Any]] = []
|
||
for facility_node_id, total, occupied in rows:
|
||
total_int = int(total or 0)
|
||
occupied_int = int(occupied or 0)
|
||
result.append(
|
||
{
|
||
"facilityNodeId": facility_node_id,
|
||
"total": total_int,
|
||
"occupied": occupied_int,
|
||
"empty": total_int - occupied_int,
|
||
}
|
||
)
|
||
return result
|
||
|
||
|
||
def list_slots(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
tenant_no: Optional[str] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
q = db.query(BizStorageFacilitySlot).filter(
|
||
and_(
|
||
BizStorageFacilitySlot.facility_node_id == facility_node_id,
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizStorageFacilitySlot.tenant_no == tenant_no)
|
||
rows = q.order_by(BizStorageFacilitySlot.order_num.asc(), BizStorageFacilitySlot.create_time.asc()).all()
|
||
facility_by_id = load_facility_map(db, tenant_no or "") if tenant_no else {}
|
||
items = [_slot_to_dict(x, facility_by_id) for x in rows]
|
||
if tenant_no:
|
||
from app.crud import storage_occupancy as occupancy_svc
|
||
|
||
items = occupancy_svc.enrich_slots_with_stored_cell_products(db, tenant_no, items)
|
||
return items
|
||
|
||
|
||
def _is_slot_available_row(row: Dict[str, Any]) -> bool:
|
||
status = row.get("slotStatus") or ""
|
||
occupied = int(row.get("occupiedCount") or 0)
|
||
return status == "空" or occupied == 0
|
||
|
||
|
||
def _slot_select_display_name(row: Dict[str, Any]) -> str:
|
||
full = (row.get("fullSlotCode") or "").strip()
|
||
code = (row.get("slotCode") or "").strip()
|
||
return full or code or str(row.get("id") or "")
|
||
|
||
|
||
def _slot_available_filter():
|
||
"""空格子:状态为「空」或 occupied_count 为 0 / NULL。"""
|
||
return or_(
|
||
BizStorageFacilitySlot.slot_status == "空",
|
||
BizStorageFacilitySlot.occupied_count == 0,
|
||
BizStorageFacilitySlot.occupied_count.is_(None),
|
||
)
|
||
|
||
|
||
def _facility_node_ids_matching_keyword(
|
||
db: Session,
|
||
tenant_no: str,
|
||
keyword: str,
|
||
) -> set[str]:
|
||
like = f"%{keyword.strip()}%"
|
||
return {
|
||
row[0]
|
||
for row in db.query(BizStorageFacility.id)
|
||
.filter(
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
BizStorageFacility.del_flag == 0,
|
||
or_(
|
||
BizStorageFacility.node_name.like(like),
|
||
BizStorageFacility.node_code.like(like),
|
||
),
|
||
)
|
||
.all()
|
||
}
|
||
|
||
|
||
def _slot_matches_select_keyword(
|
||
slot: BizStorageFacilitySlot,
|
||
keyword: str,
|
||
facility_by_id: Dict[str, BizStorageFacility],
|
||
matching_node_ids: set[str],
|
||
) -> bool:
|
||
"""下拉关键词:完整 fullSlotCode 子串 + 备注 + 设施节点名/编号。"""
|
||
kw = keyword.strip().lower()
|
||
if not kw:
|
||
return True
|
||
row = _slot_to_dict(slot, facility_by_id)
|
||
display = _slot_select_display_name(row).lower()
|
||
if kw in display:
|
||
return True
|
||
remark = (row.get("remark") or "").lower()
|
||
if kw in remark:
|
||
return True
|
||
if slot.facility_node_id in matching_node_ids:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _leaf_node_ids_for_path_prefix(
|
||
facility_by_id: Dict[str, BizStorageFacility],
|
||
path_prefix: str,
|
||
) -> set[str]:
|
||
"""按设施 node_code 路径前缀/子串匹配叶子节点(储物格挂在节点上)。"""
|
||
prefix = (path_prefix or "").strip().lower()
|
||
if not prefix:
|
||
return set()
|
||
result: set[str] = set()
|
||
for node_id in facility_by_id:
|
||
path = build_node_code_path(node_id, facility_by_id).lower()
|
||
if not path:
|
||
continue
|
||
if (
|
||
prefix in path
|
||
or path in prefix
|
||
or path.startswith(f"{prefix}-")
|
||
or prefix.startswith(f"{path}-")
|
||
):
|
||
result.add(node_id)
|
||
return result
|
||
|
||
|
||
def _build_node_path_cache(
|
||
facility_by_id: Dict[str, BizStorageFacility],
|
||
) -> Dict[str, str]:
|
||
return {
|
||
node_id: build_node_code_path(node_id, facility_by_id)
|
||
for node_id in facility_by_id
|
||
}
|
||
|
||
|
||
def _exact_node_ids_for_path(
|
||
path_cache: Dict[str, str],
|
||
path: str,
|
||
) -> List[str]:
|
||
target = (path or "").strip().lower()
|
||
if not target:
|
||
return []
|
||
return [
|
||
node_id
|
||
for node_id, node_path in path_cache.items()
|
||
if (node_path or "").strip().lower() == target
|
||
]
|
||
|
||
|
||
def _collect_slot_host_node_ids(
|
||
path_cache: Dict[str, str],
|
||
facility_by_id: Dict[str, BizStorageFacility],
|
||
path: str,
|
||
) -> List[str]:
|
||
"""储物格只挂在叶子节点;路径命中非叶子(如 1-1 架子)时展开其下全部叶子层。"""
|
||
target = (path or "").strip().lower()
|
||
if not target:
|
||
return []
|
||
|
||
anchor_ids = _exact_node_ids_for_path(path_cache, path)
|
||
if not anchor_ids:
|
||
return []
|
||
|
||
host_ids: List[str] = []
|
||
seen: set[str] = set()
|
||
child_prefix = f"{target}-"
|
||
|
||
for anchor_id in anchor_ids:
|
||
anchor = facility_by_id.get(anchor_id)
|
||
if anchor and int(getattr(anchor, "is_leaf", 0) or 0) == 1:
|
||
if anchor_id not in seen:
|
||
seen.add(anchor_id)
|
||
host_ids.append(anchor_id)
|
||
continue
|
||
|
||
for node_id, node_path in path_cache.items():
|
||
if node_id in seen:
|
||
continue
|
||
node = facility_by_id.get(node_id)
|
||
if not node or int(getattr(node, "is_leaf", 0) or 0) != 1:
|
||
continue
|
||
np = (node_path or "").strip().lower()
|
||
if np.startswith(child_prefix):
|
||
seen.add(node_id)
|
||
host_ids.append(node_id)
|
||
|
||
return host_ids
|
||
|
||
|
||
def _resolve_path_keyword(
|
||
path_cache: Dict[str, str],
|
||
keyword: str,
|
||
) -> tuple[Optional[str], Optional[str]]:
|
||
"""
|
||
解析路径型储物格编号关键词。
|
||
- '1-1-2-' -> ('1-1-2', None) 整盒
|
||
- '1-1-2-1' -> ('1-1-2', '1') 精确格位
|
||
- '1-1-2' -> ('1-1-2', None) 若存在该节点路径
|
||
"""
|
||
kw = (keyword or "").strip()
|
||
if not kw or "-" not in kw:
|
||
return None, None
|
||
|
||
if kw.endswith("-"):
|
||
path = kw.rstrip("-").strip()
|
||
if path and _exact_node_ids_for_path(path_cache, path):
|
||
return path, None
|
||
return None, None
|
||
|
||
known_paths = {
|
||
(path or "").strip().lower()
|
||
for path in path_cache.values()
|
||
if (path or "").strip()
|
||
}
|
||
parts = kw.split("-")
|
||
for i in range(len(parts), 0, -1):
|
||
candidate = "-".join(parts[:i]).strip()
|
||
if not candidate:
|
||
continue
|
||
if candidate.lower() not in known_paths:
|
||
continue
|
||
slot_part = "-".join(parts[i:]).strip() if i < len(parts) else ""
|
||
return candidate, slot_part or None
|
||
|
||
return None, None
|
||
|
||
|
||
def _build_slot_keyword_sql_filters(
|
||
db: Session,
|
||
tenant_no: str,
|
||
keyword: str,
|
||
facility_by_id: Dict[str, BizStorageFacility],
|
||
matching_node_ids: set[str],
|
||
):
|
||
"""SQL 预筛:slot_code / remark / 设施节点 + 路径型关键词分段,缩小 Python 校验范围。"""
|
||
kw = keyword.strip()
|
||
like = f"%{kw}%"
|
||
filters = [
|
||
BizStorageFacilitySlot.slot_code.like(like),
|
||
BizStorageFacilitySlot.remark.like(like),
|
||
]
|
||
if matching_node_ids:
|
||
filters.append(BizStorageFacilitySlot.facility_node_id.in_(list(matching_node_ids)))
|
||
|
||
path_cache = _build_node_path_cache(facility_by_id)
|
||
path_prefix, slot_part = _resolve_path_keyword(path_cache, kw)
|
||
if path_prefix:
|
||
host_nodes = _collect_slot_host_node_ids(path_cache, facility_by_id, path_prefix)
|
||
if host_nodes:
|
||
filters.append(BizStorageFacilitySlot.facility_node_id.in_(host_nodes))
|
||
if slot_part:
|
||
filters.append(BizStorageFacilitySlot.slot_code.like(f"{slot_part}%"))
|
||
elif "-" in kw:
|
||
prefix, loose_slot_part = kw.rsplit("-", 1)
|
||
prefix = prefix.strip()
|
||
loose_slot_part = loose_slot_part.strip()
|
||
if loose_slot_part:
|
||
filters.append(BizStorageFacilitySlot.slot_code.like(f"%{loose_slot_part}%"))
|
||
if prefix:
|
||
path_node_ids = _leaf_node_ids_for_path_prefix(facility_by_id, prefix)
|
||
if path_node_ids:
|
||
filters.append(BizStorageFacilitySlot.facility_node_id.in_(list(path_node_ids)))
|
||
return filters
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Slot select options (remote dropdown)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
SLOT_SELECT_DEFAULT_PAGE_SIZE = 20
|
||
SLOT_SELECT_MAX_PAGE_SIZE = 50
|
||
SLOT_SELECT_KEYWORD_SCAN_LIMIT = 120
|
||
|
||
|
||
def _apply_slot_select_base_filters(
|
||
query,
|
||
*,
|
||
available_only: bool = False,
|
||
include_slot_ids: Optional[List[str]] = None,
|
||
):
|
||
include_set = {
|
||
str(x).strip()
|
||
for x in (include_slot_ids or [])
|
||
if str(x).strip()
|
||
}
|
||
filtered = query
|
||
if available_only:
|
||
availability = _slot_available_filter()
|
||
if include_set:
|
||
filtered = filtered.filter(
|
||
or_(BizStorageFacilitySlot.id.in_(list(include_set)), availability)
|
||
)
|
||
else:
|
||
filtered = filtered.filter(availability)
|
||
return filtered
|
||
|
||
|
||
def _search_slot_select_matched_ids(
|
||
db: Session,
|
||
filtered,
|
||
tenant_no: str,
|
||
keyword: str,
|
||
*,
|
||
max_results: int,
|
||
) -> List[str]:
|
||
"""关键词匹配储物格 ID(限量扫描,路径型编码优先精确匹配)。"""
|
||
kw = (keyword or "").strip()
|
||
if not kw or max_results <= 0:
|
||
return []
|
||
|
||
facility_by_id = load_facility_map(db, tenant_no)
|
||
path_cache = _build_node_path_cache(facility_by_id)
|
||
matching_node_ids = _facility_node_ids_matching_keyword(db, tenant_no, kw)
|
||
|
||
matched_ids: List[str] = []
|
||
seen: set[str] = set()
|
||
|
||
def collect(rows: List[BizStorageFacilitySlot]) -> bool:
|
||
for slot in rows:
|
||
sid = slot.id
|
||
if sid in seen:
|
||
continue
|
||
if _slot_matches_select_keyword(slot, kw, facility_by_id, matching_node_ids):
|
||
seen.add(sid)
|
||
matched_ids.append(sid)
|
||
if len(matched_ids) >= max_results:
|
||
return True
|
||
return False
|
||
|
||
order_clause = (
|
||
BizStorageFacilitySlot.order_num.asc(),
|
||
BizStorageFacilitySlot.create_time.asc(),
|
||
)
|
||
|
||
# 快路径:路径型编码 1-1-2- / 1-1-2 / 1-1-2-1 → 设施路径 + 可选格位前缀(非叶子展开子层)
|
||
path_prefix, slot_part = _resolve_path_keyword(path_cache, kw)
|
||
if path_prefix:
|
||
host_nodes = _collect_slot_host_node_ids(path_cache, facility_by_id, path_prefix)
|
||
if host_nodes:
|
||
exact_q = filtered.filter(
|
||
BizStorageFacilitySlot.facility_node_id.in_(host_nodes)
|
||
)
|
||
if slot_part:
|
||
exact_q = exact_q.filter(
|
||
BizStorageFacilitySlot.slot_code.like(f"{slot_part}%")
|
||
)
|
||
exact_rows = (
|
||
exact_q.order_by(*order_clause)
|
||
.limit(max(max_results * 2, 10))
|
||
.all()
|
||
)
|
||
if collect(exact_rows):
|
||
return matched_ids
|
||
if matched_ids:
|
||
return matched_ids
|
||
|
||
keyword_filters = _build_slot_keyword_sql_filters(
|
||
db, tenant_no, kw, facility_by_id, matching_node_ids
|
||
)
|
||
sql_q = filtered.filter(or_(*keyword_filters)).order_by(*order_clause)
|
||
|
||
offset = 0
|
||
batch_size = 40
|
||
while len(matched_ids) < max_results and offset < SLOT_SELECT_KEYWORD_SCAN_LIMIT:
|
||
rows = sql_q.offset(offset).limit(batch_size).all()
|
||
if not rows:
|
||
break
|
||
if collect(rows):
|
||
return matched_ids
|
||
offset += batch_size
|
||
|
||
return matched_ids
|
||
|
||
|
||
def search_slot_ids_by_keyword(
|
||
db: Session,
|
||
tenant_no: str,
|
||
keyword: str,
|
||
*,
|
||
max_results: int = SLOT_SELECT_KEYWORD_SCAN_LIMIT,
|
||
) -> List[str]:
|
||
"""按路径/格位编码关键词解析储物格 ID(供细胞产品列表等跨实体检索复用)。"""
|
||
kw = (keyword or "").strip()
|
||
if not kw or max_results <= 0:
|
||
return []
|
||
base = db.query(BizStorageFacilitySlot).filter(
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||
)
|
||
return _search_slot_select_matched_ids(
|
||
db,
|
||
base,
|
||
tenant_no,
|
||
kw,
|
||
max_results=max_results,
|
||
)
|
||
|
||
|
||
def _apply_slot_select_query_filters(
|
||
db: Session,
|
||
query,
|
||
*,
|
||
tenant_no: str,
|
||
keyword: Optional[str] = None,
|
||
available_only: bool = False,
|
||
include_slot_ids: Optional[List[str]] = None,
|
||
):
|
||
"""兼容旧调用:无关键词时等同 base;有关键词时返回 ID 过滤后的 query。"""
|
||
filtered = _apply_slot_select_base_filters(
|
||
query,
|
||
available_only=available_only,
|
||
include_slot_ids=include_slot_ids,
|
||
)
|
||
kw = (keyword or "").strip()
|
||
if not kw:
|
||
return filtered
|
||
matched_ids = _search_slot_select_matched_ids(
|
||
db,
|
||
filtered,
|
||
tenant_no,
|
||
kw,
|
||
max_results=SLOT_SELECT_KEYWORD_SCAN_LIMIT,
|
||
)
|
||
if not matched_ids:
|
||
return filtered.filter(false())
|
||
return filtered.filter(BizStorageFacilitySlot.id.in_(matched_ids))
|
||
|
||
|
||
def _load_slot_select_include_rows(query, include_slot_ids: Optional[List[str]]):
|
||
"""按传入顺序加载需强制保留的储物格(编辑态当前选中格)。"""
|
||
include_ids = [
|
||
str(x).strip()
|
||
for x in (include_slot_ids or [])
|
||
if str(x).strip()
|
||
]
|
||
if not include_ids:
|
||
return []
|
||
rows_by_id = {
|
||
row.id: row
|
||
for row in query.filter(BizStorageFacilitySlot.id.in_(include_ids)).all()
|
||
}
|
||
return [rows_by_id[sid] for sid in include_ids if sid in rows_by_id]
|
||
|
||
|
||
def _merge_slot_select_rows_with_includes(
|
||
page_rows: List[BizStorageFacilitySlot],
|
||
include_rows: List[BizStorageFacilitySlot],
|
||
) -> List[BizStorageFacilitySlot]:
|
||
"""将 include 格置顶合并进当前页,避免分页把当前选中格挤出导致下拉显示 id。"""
|
||
merged: List[BizStorageFacilitySlot] = []
|
||
seen: set[str] = set()
|
||
for row in list(include_rows or []) + list(page_rows or []):
|
||
sid = row.id
|
||
if not sid or sid in seen:
|
||
continue
|
||
seen.add(sid)
|
||
merged.append(row)
|
||
return merged
|
||
|
||
|
||
def list_slot_select_options(
|
||
db: Session,
|
||
*,
|
||
query,
|
||
tenant_no: str,
|
||
keyword: Optional[str] = None,
|
||
available_only: bool = False,
|
||
include_slot_ids: Optional[List[str]] = None,
|
||
page: int = 1,
|
||
page_size: int = SLOT_SELECT_DEFAULT_PAGE_SIZE,
|
||
) -> tuple[List[Dict[str, Any]], int]:
|
||
"""储物格下拉远程选项:默认 20 条;关键词检索限量扫描 + 路径快路径。"""
|
||
page = max(int(page or 1), 1)
|
||
page_size = max(
|
||
min(int(page_size or SLOT_SELECT_DEFAULT_PAGE_SIZE), SLOT_SELECT_MAX_PAGE_SIZE),
|
||
1,
|
||
)
|
||
need_count = page * page_size
|
||
include_rows = _load_slot_select_include_rows(query, include_slot_ids)
|
||
|
||
filtered = _apply_slot_select_base_filters(
|
||
query,
|
||
available_only=available_only,
|
||
include_slot_ids=include_slot_ids,
|
||
)
|
||
kw = (keyword or "").strip()
|
||
order_clause = (
|
||
BizStorageFacilitySlot.order_num.asc(),
|
||
BizStorageFacilitySlot.create_time.asc(),
|
||
)
|
||
|
||
if kw:
|
||
matched_ids = _search_slot_select_matched_ids(
|
||
db,
|
||
filtered,
|
||
tenant_no,
|
||
kw,
|
||
max_results=need_count,
|
||
)
|
||
total = len(matched_ids)
|
||
page_ids = matched_ids[(page - 1) * page_size : page * page_size]
|
||
rows_by_id = {
|
||
row.id: row
|
||
for row in (
|
||
filtered.filter(BizStorageFacilitySlot.id.in_(page_ids)).all()
|
||
if page_ids
|
||
else []
|
||
)
|
||
}
|
||
rows = [rows_by_id[sid] for sid in page_ids if sid in rows_by_id]
|
||
else:
|
||
total = (
|
||
filtered.order_by(None)
|
||
.with_entities(func.count(BizStorageFacilitySlot.id))
|
||
.scalar()
|
||
or 0
|
||
)
|
||
rows = (
|
||
filtered.order_by(*order_clause)
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
|
||
rows = _merge_slot_select_rows_with_includes(rows, include_rows)
|
||
if not rows:
|
||
return [], int(total)
|
||
|
||
facility_by_id = load_facility_map(db, tenant_no)
|
||
result_list = []
|
||
for slot_row in rows:
|
||
row = _slot_to_dict(slot_row, facility_by_id)
|
||
result_list.append(
|
||
{
|
||
"id": row.get("id"),
|
||
"name": _slot_select_display_name(row),
|
||
"fullSlotCode": row.get("fullSlotCode") or "",
|
||
"slotCode": row.get("slotCode") or "",
|
||
"facilityNodeId": row.get("facilityNodeId") or "",
|
||
"slotStatus": row.get("slotStatus") or "",
|
||
"occupiedCount": int(row.get("occupiedCount") or 0),
|
||
}
|
||
)
|
||
return result_list, int(total)
|
||
|
||
|
||
def get_slot(db: Session, slot_id: str, tenant_no: Optional[str] = None) -> Optional[BizStorageFacilitySlot]:
|
||
q = db.query(BizStorageFacilitySlot).filter(
|
||
and_(BizStorageFacilitySlot.id == slot_id, BizStorageFacilitySlot.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizStorageFacilitySlot.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def _slot_to_dict_for_row(db: Session, s: BizStorageFacilitySlot) -> Dict[str, Any]:
|
||
facility_by_id = load_facility_map(db, s.tenant_no)
|
||
return _slot_to_dict(s, facility_by_id)
|
||
|
||
|
||
def create_slot(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
slot_code: Optional[str] = None,
|
||
slot_status: Optional[str] = None,
|
||
tenant_no: str = "zkxw",
|
||
order_num: Optional[int] = None,
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
at_row_num: Optional[int] = None,
|
||
) -> BizStorageFacilitySlot:
|
||
fid = (facility_node_id or "").strip()
|
||
if not fid:
|
||
raise ValueError("所属设施节点不能为空")
|
||
if at_row_num is not None:
|
||
return insert_slot_at_row_end(
|
||
db,
|
||
facility_node_id=fid,
|
||
row_num=at_row_num,
|
||
slot_status=slot_status,
|
||
tenant_no=tenant_no,
|
||
remark=remark,
|
||
user_id=user_id,
|
||
)
|
||
obj = BizStorageFacilitySlot(
|
||
id=generate_uuid(),
|
||
facility_node_id=fid,
|
||
slot_code=(slot_code or "").strip() or None,
|
||
slot_status=(slot_status or "").strip() or None,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num if order_num is not None else 0,
|
||
remark=(remark or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def insert_slot_at_row_end(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
row_num: int,
|
||
slot_status: Optional[str] = None,
|
||
tenant_no: str = "zkxw",
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> BizStorageFacilitySlot:
|
||
fid = (facility_node_id or "").strip()
|
||
row = int(row_num)
|
||
if row < 1:
|
||
raise ValueError("行号参数不合法")
|
||
|
||
node = db.query(BizStorageFacility).filter(
|
||
and_(BizStorageFacility.id == fid, BizStorageFacility.del_flag == 0)
|
||
).first()
|
||
if not node:
|
||
raise ValueError("所属设施节点不存在")
|
||
if not node.is_leaf:
|
||
raise ValueError("只能为叶子节点创建格子")
|
||
|
||
max_col = 0
|
||
for slot in _list_facility_slots(db, fid, tenant_no):
|
||
slot_row, slot_col = _resolve_slot_layout_position(slot)
|
||
if slot_row == row:
|
||
max_col = max(max_col, slot_col)
|
||
_ensure_slot_layout_order(slot, user_id=user_id)
|
||
|
||
now = datetime.now()
|
||
obj = BizStorageFacilitySlot(
|
||
id=generate_uuid(),
|
||
facility_node_id=fid,
|
||
slot_code=None,
|
||
slot_status=(slot_status or "空").strip() or "空",
|
||
tenant_no=tenant_no,
|
||
order_num=encode_slot_layout_order(row, max_col + 1),
|
||
remark=(remark or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_slot(
|
||
db: Session,
|
||
slot_id: str,
|
||
*,
|
||
slot_code: Optional[str] = None,
|
||
slot_status: Optional[str] = None,
|
||
order_num: Optional[int] = None,
|
||
remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
tenant_no: Optional[str] = None,
|
||
) -> BizStorageFacilitySlot:
|
||
obj = get_slot(db, slot_id, tenant_no=tenant_no)
|
||
if not obj:
|
||
raise ValueError("存储设施小格不存在")
|
||
old_slot_code = (obj.slot_code or "").strip()
|
||
if slot_code is not None:
|
||
obj.slot_code = (slot_code or "").strip() or None
|
||
if slot_status is not None:
|
||
obj.slot_status = (slot_status or "").strip() or None
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = (remark or "").strip() or None
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
|
||
slot_code_changed = slot_code is not None and (obj.slot_code or "").strip() != old_slot_code
|
||
if slot_code_changed:
|
||
cascade_refresh_slot_name_snapshots_for_slots(db, [slot_id], obj.tenant_no)
|
||
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_slot(db: Session, slot_id: str, tenant_no: Optional[str] = None, user_id: Optional[str] = None) -> None:
|
||
obj = get_slot(db, slot_id, tenant_no=tenant_no)
|
||
if not obj:
|
||
raise ValueError("存储设施小格不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
obj.update_by = user_id
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
def clear_slots_by_facility_node(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> int:
|
||
fid = (facility_node_id or "").strip()
|
||
if not fid:
|
||
raise ValueError("所属设施节点不能为空")
|
||
node = db.query(BizStorageFacility).filter(
|
||
and_(
|
||
BizStorageFacility.id == fid,
|
||
BizStorageFacility.del_flag == 0,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
)
|
||
).first()
|
||
if not node:
|
||
raise ValueError("所属设施节点不存在")
|
||
now = datetime.now()
|
||
updated = db.query(BizStorageFacilitySlot).filter(
|
||
BizStorageFacilitySlot.facility_node_id == fid,
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||
).update(
|
||
{"del_flag": 1, "update_time": now, "update_by": user_id},
|
||
synchronize_session=False
|
||
)
|
||
db.commit()
|
||
return int(updated or 0)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Slot layout & sequential code helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _resolve_slot_layout_position(slot: BizStorageFacilitySlot) -> tuple[int, int]:
|
||
order = int(slot.order_num or 0)
|
||
row, col = decode_slot_layout_order(order)
|
||
if row > 0:
|
||
return row, col
|
||
match = re.match(r"^(.*?)(\d+)-(\d+)$", (slot.slot_code or "").strip())
|
||
if match:
|
||
return int(match.group(2)), int(match.group(3))
|
||
return 0, 0
|
||
|
||
|
||
def _ensure_slot_layout_order(
|
||
slot: BizStorageFacilitySlot,
|
||
*,
|
||
user_id: Optional[str] = None,
|
||
) -> tuple[int, int]:
|
||
row, col = _resolve_slot_layout_position(slot)
|
||
if row > 0:
|
||
expected = encode_slot_layout_order(row, col)
|
||
if int(slot.order_num or 0) != expected:
|
||
slot.order_num = expected
|
||
if user_id:
|
||
slot.update_by = user_id
|
||
slot.update_time = datetime.now()
|
||
return row, col
|
||
return 0, 0
|
||
|
||
|
||
def _list_facility_slots(
|
||
db: Session,
|
||
facility_node_id: str,
|
||
tenant_no: str,
|
||
) -> List[BizStorageFacilitySlot]:
|
||
return (
|
||
db.query(BizStorageFacilitySlot)
|
||
.filter(
|
||
BizStorageFacilitySlot.facility_node_id == facility_node_id,
|
||
BizStorageFacilitySlot.del_flag == 0,
|
||
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||
)
|
||
.order_by(
|
||
BizStorageFacilitySlot.order_num.asc(),
|
||
BizStorageFacilitySlot.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
|
||
|
||
def batch_create_slots(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
rows: int,
|
||
cols: int,
|
||
tenant_no: str = "zkxw",
|
||
user_id: Optional[str] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
fid = (facility_node_id or "").strip()
|
||
if not fid:
|
||
raise ValueError("所属设施节点不能为空")
|
||
if rows < 1 or cols < 1:
|
||
raise ValueError("行数和列数必须大于 0")
|
||
if rows > 50 or cols > 50:
|
||
raise ValueError("单次最多生成 50×50 = 2500 个格子")
|
||
node = db.query(BizStorageFacility).filter(
|
||
and_(BizStorageFacility.id == fid, BizStorageFacility.del_flag == 0)
|
||
).first()
|
||
if not node:
|
||
raise ValueError("所属设施节点不存在")
|
||
if not node.is_leaf:
|
||
raise ValueError("只能为叶子节点创建格子")
|
||
now = datetime.now()
|
||
slots: List[BizStorageFacilitySlot] = []
|
||
seq = 0
|
||
for r in range(1, rows + 1):
|
||
for c in range(1, cols + 1):
|
||
seq += 1
|
||
obj = BizStorageFacilitySlot(
|
||
id=generate_uuid(),
|
||
facility_node_id=fid,
|
||
slot_code=str(seq),
|
||
slot_status="空",
|
||
tenant_no=tenant_no,
|
||
order_num=encode_slot_layout_order(r, c),
|
||
remark=None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
slots.append(obj)
|
||
db.commit()
|
||
facility_by_id = load_facility_map(db, tenant_no)
|
||
for s in slots:
|
||
db.refresh(s)
|
||
return [_slot_to_dict(s, facility_by_id) for s in slots]
|
||
|
||
|
||
def insert_row_and_shift_slots(
|
||
db: Session,
|
||
*,
|
||
facility_node_id: str,
|
||
after_row_num: int,
|
||
cols: int,
|
||
tenant_no: str = "zkxw",
|
||
user_id: Optional[str] = None,
|
||
) -> Dict[str, int]:
|
||
fid = (facility_node_id or "").strip()
|
||
if not fid:
|
||
raise ValueError("所属设施节点不能为空")
|
||
if after_row_num < 0:
|
||
raise ValueError("插入行号参数不合法")
|
||
if cols < 1:
|
||
raise ValueError("列数必须大于 0")
|
||
|
||
node = db.query(BizStorageFacility).filter(
|
||
and_(
|
||
BizStorageFacility.id == fid,
|
||
BizStorageFacility.del_flag == 0,
|
||
BizStorageFacility.tenant_no == tenant_no,
|
||
)
|
||
).first()
|
||
if not node:
|
||
raise ValueError("所属设施节点不存在")
|
||
if not node.is_leaf:
|
||
raise ValueError("只能为叶子节点新增格子行")
|
||
|
||
slots = _list_facility_slots(db, fid, tenant_no)
|
||
|
||
now = datetime.now()
|
||
shifted_count = 0
|
||
for slot in slots:
|
||
row_no, col_no = _ensure_slot_layout_order(slot, user_id=user_id)
|
||
if row_no <= 0:
|
||
continue
|
||
if row_no > after_row_num:
|
||
slot.order_num = encode_slot_layout_order(row_no + 1, col_no)
|
||
slot.update_by = user_id
|
||
slot.update_time = now
|
||
shifted_count += 1
|
||
|
||
insert_row_num = after_row_num + 1
|
||
created_slots: List[BizStorageFacilitySlot] = []
|
||
for col in range(1, cols + 1):
|
||
created_slots.append(
|
||
BizStorageFacilitySlot(
|
||
id=generate_uuid(),
|
||
facility_node_id=fid,
|
||
slot_code=None,
|
||
slot_status="空",
|
||
tenant_no=tenant_no,
|
||
order_num=encode_slot_layout_order(insert_row_num, col),
|
||
remark=None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
if created_slots:
|
||
db.add_all(created_slots)
|
||
db.commit()
|
||
return {"shiftedCount": shifted_count, "createdCount": len(created_slots)}
|