Files
zkxw/backend/app/crud/storage_facility.py
T
2026-06-25 12:41:07 +08:00

730 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""存储设施(树结构)+ 小格 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_, 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,
cascade_refresh_slot_name_snapshots_for_nodes,
cascade_refresh_slot_name_snapshots_for_slots,
decode_slot_layout_order,
encode_slot_layout_order,
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 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 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 {}
return [_slot_to_dict(x, facility_by_id) for x in rows]
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 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 = 100,
) -> tuple[List[Dict[str, Any]], int]:
"""Paginated storage slot options for remote select; keyword matches code/path/name."""
include_set = {
str(x).strip()
for x in (include_slot_ids or [])
if str(x).strip()
}
kw = (keyword or "").strip().lower()
facility_by_id = load_facility_map(db, tenant_no)
rows = query.order_by(
BizStorageFacilitySlot.order_num.asc(),
BizStorageFacilitySlot.create_time.asc(),
).all()
enriched: List[Dict[str, Any]] = []
for slot_row in rows:
row = _slot_to_dict(slot_row, facility_by_id)
slot_id = str(row.get("id") or "").strip()
if not slot_id:
continue
if available_only and slot_id not in include_set and not _is_slot_available_row(row):
continue
if kw:
node = facility_by_id.get(str(row.get("facilityNodeId") or "").strip())
haystack = " ".join(
[
str(row.get("slotCode") or ""),
str(row.get("fullSlotCode") or ""),
str(row.get("remark") or ""),
str(getattr(node, "node_name", "") or "") if node else "",
str(getattr(node, "node_code", "") or "") if node else "",
]
).lower()
if kw not in haystack:
continue
row["name"] = _slot_select_display_name(row)
enriched.append(row)
enriched.sort(
key=lambda x: (
str(x.get("fullSlotCode") or x.get("slotCode") or ""),
str(x.get("id") or ""),
)
)
total = len(enriched)
page = max(int(page or 1), 1)
page_size = max(min(int(page_size or 100), 200), 1)
start = (page - 1) * page_size
page_rows = enriched[start : start + page_size]
result_list = [
{
"id": row.get("id"),
"name": row.get("name") or _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),
}
for row in page_rows
]
return result_list, 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)}