storage_facility_code.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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 collect_slot_ids_under_nodes(
  101. db: Session,
  102. node_ids: List[str],
  103. tenant_no: str,
  104. *,
  105. collect_descendant_ids_fn,
  106. ) -> List[str]:
  107. all_node_ids: set[str] = set()
  108. for nid in node_ids:
  109. nid = (nid or "").strip()
  110. if not nid:
  111. continue
  112. all_node_ids.add(nid)
  113. all_node_ids.update(collect_descendant_ids_fn(db, nid, tenant_no))
  114. if not all_node_ids:
  115. return []
  116. leaves = (
  117. db.query(BizStorageFacility)
  118. .filter(
  119. BizStorageFacility.id.in_(all_node_ids),
  120. BizStorageFacility.is_leaf == 1,
  121. BizStorageFacility.tenant_no == tenant_no,
  122. BizStorageFacility.del_flag == 0,
  123. )
  124. .all()
  125. )
  126. leaf_ids = [leaf.id for leaf in leaves]
  127. if not leaf_ids:
  128. return []
  129. slots = (
  130. db.query(BizStorageFacilitySlot)
  131. .filter(
  132. BizStorageFacilitySlot.facility_node_id.in_(leaf_ids),
  133. BizStorageFacilitySlot.tenant_no == tenant_no,
  134. BizStorageFacilitySlot.del_flag == 0,
  135. )
  136. .all()
  137. )
  138. return [s.id for s in slots]
  139. def refresh_outbound_release_cell_slot_names(
  140. db: Session,
  141. slot_full_map: Dict[str, str],
  142. tenant_no: str,
  143. ) -> int:
  144. if not slot_full_map:
  145. return 0
  146. products = (
  147. db.query(BizCellProduct)
  148. .filter(
  149. BizCellProduct.storage_slot_id.in_(list(slot_full_map.keys())),
  150. BizCellProduct.tenant_no == tenant_no,
  151. BizCellProduct.del_flag == 0,
  152. )
  153. .all()
  154. )
  155. product_name_map = {
  156. p.id: slot_full_map.get((p.storage_slot_id or "").strip(), "")
  157. for p in products
  158. if (p.storage_slot_id or "").strip()
  159. }
  160. if not product_name_map:
  161. return 0
  162. cells = (
  163. db.query(BizOutboundReleaseCell)
  164. .filter(
  165. BizOutboundReleaseCell.cell_product_id.in_(list(product_name_map.keys())),
  166. BizOutboundReleaseCell.tenant_no == tenant_no,
  167. BizOutboundReleaseCell.del_flag == 0,
  168. )
  169. .all()
  170. )
  171. now = datetime.now()
  172. updated = 0
  173. for cell in cells:
  174. new_name = product_name_map.get(cell.cell_product_id, "")
  175. if new_name and cell.storage_slot_name != new_name:
  176. cell.storage_slot_name = new_name
  177. cell.update_time = now
  178. updated += 1
  179. return updated
  180. def cascade_refresh_slot_name_snapshots_for_slots(
  181. db: Session,
  182. slot_ids: List[str],
  183. tenant_no: str,
  184. ) -> int:
  185. ids = [(sid or "").strip() for sid in slot_ids if (sid or "").strip()]
  186. if not ids:
  187. return 0
  188. slots = (
  189. db.query(BizStorageFacilitySlot)
  190. .filter(
  191. BizStorageFacilitySlot.id.in_(ids),
  192. BizStorageFacilitySlot.tenant_no == tenant_no,
  193. BizStorageFacilitySlot.del_flag == 0,
  194. )
  195. .all()
  196. )
  197. if not slots:
  198. return 0
  199. facility_by_id = load_facility_map(db, tenant_no)
  200. slot_full_map = {
  201. s.id: build_full_slot_code(s.facility_node_id, s.slot_code, facility_by_id)
  202. for s in slots
  203. }
  204. return refresh_outbound_release_cell_slot_names(db, slot_full_map, tenant_no)
  205. def cascade_refresh_slot_name_snapshots_for_nodes(
  206. db: Session,
  207. node_ids: List[str],
  208. tenant_no: str,
  209. *,
  210. collect_descendant_ids_fn,
  211. ) -> int:
  212. slot_ids = collect_slot_ids_under_nodes(
  213. db, node_ids, tenant_no, collect_descendant_ids_fn=collect_descendant_ids_fn
  214. )
  215. return cascade_refresh_slot_name_snapshots_for_slots(db, slot_ids, tenant_no)