storage_facility_code.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """库位节点编号路径与储物格完整编号计算、快照级联刷新。"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Dict, List, Optional, Union
  5. from sqlalchemy.orm import Session
  6. from app.models.models import (
  7. BizCellProduct,
  8. BizOutboundReleaseCell,
  9. BizStorageFacility,
  10. BizStorageFacilitySlot,
  11. )
  12. SLOT_LAYOUT_ROW_STRIDE = 1000
  13. def encode_slot_layout_order(row: int, col: int) -> int:
  14. """将货架行列位置编码为 order_num(row*1000+col)。"""
  15. row_i = int(row)
  16. col_i = int(col)
  17. if row_i < 1 or col_i < 1:
  18. raise ValueError("行列序号必须从 1 开始")
  19. if col_i >= SLOT_LAYOUT_ROW_STRIDE:
  20. raise ValueError(f"列数不能超过 {SLOT_LAYOUT_ROW_STRIDE - 1}")
  21. return row_i * SLOT_LAYOUT_ROW_STRIDE + col_i
  22. def decode_slot_layout_order(order_num: int) -> tuple[int, int]:
  23. """解析 order_num 中的行列位置;非布局编码时返回 (0, order)。"""
  24. order = int(order_num or 0)
  25. if order >= SLOT_LAYOUT_ROW_STRIDE:
  26. return order // SLOT_LAYOUT_ROW_STRIDE, order % SLOT_LAYOUT_ROW_STRIDE
  27. return 0, order
  28. def validate_node_code(node_code: str) -> str:
  29. code = (node_code or "").strip()
  30. if not code:
  31. return ""
  32. if len(code) > 32:
  33. raise ValueError("节点编号长度不能超过 32 个字符")
  34. return code
  35. def assert_sibling_node_code_unique(
  36. db: Session,
  37. *,
  38. parent_id: str,
  39. node_code: str,
  40. tenant_no: str,
  41. exclude_id: Optional[str] = None,
  42. ) -> None:
  43. pid = (parent_id or "0").strip() or "0"
  44. code = validate_node_code(node_code)
  45. if not code:
  46. return
  47. q = db.query(BizStorageFacility).filter(
  48. BizStorageFacility.parent_id == pid,
  49. BizStorageFacility.node_code == code,
  50. BizStorageFacility.tenant_no == tenant_no,
  51. BizStorageFacility.del_flag == 0,
  52. )
  53. if exclude_id:
  54. q = q.filter(BizStorageFacility.id != exclude_id)
  55. if q.first():
  56. raise ValueError("同级节点编号已存在")
  57. def build_node_code_path(
  58. node_id: str,
  59. facility_by_id: Dict[str, Union[BizStorageFacility, dict]],
  60. ) -> str:
  61. parts: List[str] = []
  62. cur_id = (node_id or "").strip()
  63. visited: set[str] = set()
  64. while cur_id and cur_id != "0" and cur_id not in visited:
  65. visited.add(cur_id)
  66. node = facility_by_id.get(cur_id)
  67. if not node:
  68. break
  69. if isinstance(node, dict):
  70. code = (node.get("nodeCode") or node.get("node_code") or "").strip()
  71. parent_id = (node.get("parentId") or node.get("parent_id") or "0").strip()
  72. else:
  73. code = (node.node_code or "").strip()
  74. parent_id = (node.parent_id or "0").strip()
  75. if code:
  76. parts.append(code)
  77. cur_id = parent_id
  78. parts.reverse()
  79. return "-".join(parts)
  80. def build_full_slot_code(
  81. facility_node_id: str,
  82. slot_code: Optional[str],
  83. facility_by_id: Dict[str, Union[BizStorageFacility, dict]],
  84. ) -> str:
  85. path = build_node_code_path(facility_node_id, facility_by_id)
  86. local = (slot_code or "").strip()
  87. if path and local:
  88. return f"{path}-{local}"
  89. return local or path
  90. def load_facility_map(db: Session, tenant_no: str) -> Dict[str, BizStorageFacility]:
  91. rows = (
  92. db.query(BizStorageFacility)
  93. .filter(
  94. BizStorageFacility.tenant_no == tenant_no,
  95. BizStorageFacility.del_flag == 0,
  96. )
  97. .all()
  98. )
  99. return {r.id: r for r in rows}
  100. def load_facility_ancestor_map(
  101. db: Session,
  102. tenant_no: str,
  103. node_id: str,
  104. ) -> Dict[str, BizStorageFacility]:
  105. """仅加载某节点到根的祖先链(含自身),供单节点格子列表拼 fullSlotCode。"""
  106. result: Dict[str, BizStorageFacility] = {}
  107. cur_id = (node_id or "").strip()
  108. while cur_id and cur_id != "0" and cur_id not in result:
  109. row = (
  110. db.query(BizStorageFacility)
  111. .filter(
  112. BizStorageFacility.id == cur_id,
  113. BizStorageFacility.tenant_no == tenant_no,
  114. BizStorageFacility.del_flag == 0,
  115. )
  116. .first()
  117. )
  118. if not row:
  119. break
  120. result[row.id] = row
  121. cur_id = (row.parent_id or "0").strip()
  122. return result
  123. def collect_slot_ids_under_nodes(
  124. db: Session,
  125. node_ids: List[str],
  126. tenant_no: str,
  127. *,
  128. collect_descendant_ids_fn,
  129. ) -> List[str]:
  130. all_node_ids: set[str] = set()
  131. for nid in node_ids:
  132. nid = (nid or "").strip()
  133. if not nid:
  134. continue
  135. all_node_ids.add(nid)
  136. all_node_ids.update(collect_descendant_ids_fn(db, nid, tenant_no))
  137. if not all_node_ids:
  138. return []
  139. leaves = (
  140. db.query(BizStorageFacility)
  141. .filter(
  142. BizStorageFacility.id.in_(all_node_ids),
  143. BizStorageFacility.is_leaf == 1,
  144. BizStorageFacility.tenant_no == tenant_no,
  145. BizStorageFacility.del_flag == 0,
  146. )
  147. .all()
  148. )
  149. leaf_ids = [leaf.id for leaf in leaves]
  150. if not leaf_ids:
  151. return []
  152. slots = (
  153. db.query(BizStorageFacilitySlot)
  154. .filter(
  155. BizStorageFacilitySlot.facility_node_id.in_(leaf_ids),
  156. BizStorageFacilitySlot.tenant_no == tenant_no,
  157. BizStorageFacilitySlot.del_flag == 0,
  158. )
  159. .all()
  160. )
  161. return [s.id for s in slots]
  162. def refresh_outbound_release_cell_slot_names(
  163. db: Session,
  164. slot_full_map: Dict[str, str],
  165. tenant_no: str,
  166. ) -> int:
  167. if not slot_full_map:
  168. return 0
  169. products = (
  170. db.query(BizCellProduct)
  171. .filter(
  172. BizCellProduct.storage_slot_id.in_(list(slot_full_map.keys())),
  173. BizCellProduct.tenant_no == tenant_no,
  174. BizCellProduct.del_flag == 0,
  175. )
  176. .all()
  177. )
  178. product_name_map = {
  179. p.id: slot_full_map.get((p.storage_slot_id or "").strip(), "")
  180. for p in products
  181. if (p.storage_slot_id or "").strip()
  182. }
  183. if not product_name_map:
  184. return 0
  185. cells = (
  186. db.query(BizOutboundReleaseCell)
  187. .filter(
  188. BizOutboundReleaseCell.cell_product_id.in_(list(product_name_map.keys())),
  189. BizOutboundReleaseCell.tenant_no == tenant_no,
  190. BizOutboundReleaseCell.del_flag == 0,
  191. )
  192. .all()
  193. )
  194. now = datetime.now()
  195. updated = 0
  196. for cell in cells:
  197. new_name = product_name_map.get(cell.cell_product_id, "")
  198. if new_name and cell.storage_slot_name != new_name:
  199. cell.storage_slot_name = new_name
  200. cell.update_time = now
  201. updated += 1
  202. return updated
  203. def cascade_refresh_slot_name_snapshots_for_slots(
  204. db: Session,
  205. slot_ids: List[str],
  206. tenant_no: str,
  207. ) -> int:
  208. ids = [(sid or "").strip() for sid in slot_ids if (sid or "").strip()]
  209. if not ids:
  210. return 0
  211. slots = (
  212. db.query(BizStorageFacilitySlot)
  213. .filter(
  214. BizStorageFacilitySlot.id.in_(ids),
  215. BizStorageFacilitySlot.tenant_no == tenant_no,
  216. BizStorageFacilitySlot.del_flag == 0,
  217. )
  218. .all()
  219. )
  220. if not slots:
  221. return 0
  222. facility_by_id = load_facility_map(db, tenant_no)
  223. slot_full_map = {
  224. s.id: build_full_slot_code(s.facility_node_id, s.slot_code, facility_by_id)
  225. for s in slots
  226. }
  227. return refresh_outbound_release_cell_slot_names(db, slot_full_map, tenant_no)
  228. def cascade_refresh_slot_name_snapshots_for_nodes(
  229. db: Session,
  230. node_ids: List[str],
  231. tenant_no: str,
  232. *,
  233. collect_descendant_ids_fn,
  234. ) -> int:
  235. slot_ids = collect_slot_ids_under_nodes(
  236. db, node_ids, tenant_no, collect_descendant_ids_fn=collect_descendant_ids_fn
  237. )
  238. return cascade_refresh_slot_name_snapshots_for_slots(db, slot_ids, tenant_no)