views.py 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135
  1. from rest_framework import viewsets
  2. from utils.page import MyPageNumberPagination
  3. from utils.datasolve import sumOfList, transportation_calculate
  4. from utils.md5 import Md5
  5. from rest_framework.filters import OrderingFilter
  6. from django_filters.rest_framework import DjangoFilterBackend
  7. from rest_framework.response import Response
  8. from rest_framework.exceptions import APIException
  9. from django.utils import timezone
  10. from django.db import transaction
  11. import logging
  12. from rest_framework import status
  13. from .models import DeviceModel,LocationModel,LocationGroupModel,LocationContainerLink,LocationChangeLog,alloction_pre,base_location
  14. from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel
  15. from .filter import DeviceFilter,LocationFilter,LocationContainerLinkFilter,LocationChangeLogFilter,LocationGroupFilter
  16. from .serializers import LocationListSerializer,LocationPostSerializer
  17. from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer
  18. # 以后添加模块时,只需要在这里添加即可
  19. from rest_framework.permissions import AllowAny
  20. from container.models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel
  21. from django.db.models import Prefetch
  22. import copy
  23. import json
  24. from collections import defaultdict
  25. logger = logging.getLogger(__name__)
  26. from operation_log.views import log_operation,log_failure_operation,log_success_operation
  27. # 库位分配
  28. # 入库规则函数
  29. # 逻辑根据批次下的托盘数目来找满足区间范围的库位,按照优先级排序,
  30. class locationViewSet(viewsets.ModelViewSet):
  31. """
  32. retrieve:
  33. Response a data list(get)
  34. list:
  35. Response a data list(all)
  36. create:
  37. Create a data line(post)
  38. delete:
  39. Delete a data line(delete)
  40. """
  41. # authentication_classes = [] # 禁用所有认证类
  42. # permission_classes = [AllowAny] # 允许任意访问
  43. pagination_class = MyPageNumberPagination
  44. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  45. ordering_fields = ['id', "create_time", "update_time", ]
  46. filter_class = LocationFilter
  47. def get_project(self):
  48. try:
  49. id = self.kwargs.get('pk')
  50. return id
  51. except:
  52. return None
  53. def get_queryset(self):
  54. id = self.get_project()
  55. prefetch_containers = Prefetch(
  56. 'container_links',
  57. queryset=LocationContainerLink.objects.filter(
  58. is_active=True
  59. ).select_related('container'), # 加载关联的托盘对象
  60. to_attr='active_links' # 新的属性名称
  61. )
  62. if self.request.auth:
  63. if id is None:
  64. log_operation(
  65. request=self.request,
  66. operation_content="查看库位列表",
  67. operation_level="view",
  68. operator=self.request.auth.name if self.request.auth else None,
  69. module_name="库位"
  70. )
  71. return LocationModel.objects.prefetch_related(prefetch_containers).all()
  72. else:
  73. log_operation(
  74. request=self.request,
  75. operation_content=f"查看库位详情 ID:{id}",
  76. operation_level="view",
  77. operator=self.request.auth.name if self.request.auth else None,
  78. object_id=id,
  79. module_name="库位"
  80. )
  81. return LocationModel.objects.prefetch_related(prefetch_containers).filter(id=id)
  82. else:
  83. return LocationModel.objects.none()
  84. def get_serializer_class(self):
  85. if self.action == 'list':
  86. return LocationListSerializer
  87. elif self.action == 'update':
  88. return LocationPostSerializer
  89. elif self.action =='retrieve':
  90. return LocationListSerializer
  91. def update(self, request, *args, **kwargs):
  92. qs = self.get_object()
  93. data = self.request.data
  94. location_code = data.get('location_code')
  95. # 处理库位对象
  96. location_obj = LocationModel.objects.filter(location_code=location_code).first()
  97. if not location_obj:
  98. log_failure_operation(
  99. request=self.request,
  100. operation_content=f"更新库位失败,库位 {location_code} 不存在",
  101. operation_level="update",
  102. operator=self.request.auth.name if self.request.auth else None,
  103. module_name="库位"
  104. )
  105. logger.info(f"库位 {location_code} 不存在")
  106. return Response(
  107. {'code': '400', 'message': '库位不存在', 'data': None},
  108. status=status.HTTP_400_BAD_REQUEST
  109. )
  110. else:
  111. data['id'] = location_obj.id
  112. logger.info(f"库位 {location_code} 已存在")
  113. serializer = self.get_serializer(qs, data=data)
  114. serializer.is_valid(raise_exception=True)
  115. serializer.save()
  116. log_success_operation(
  117. request=self.request,
  118. operation_content=f"更新库位成功,库位 {location_code} 已更新",
  119. operation_level="update",
  120. operator=self.request.auth.name if self.request.auth else None,
  121. object_id=location_obj.id,
  122. module_name="库位"
  123. )
  124. headers = self.get_success_headers(serializer.data)
  125. self.handle_group_location_status(location_code,location_obj.location_group)
  126. return Response(serializer.data, status=200, headers=headers)
  127. def handle_group_location_status(self,location_code,location_group):
  128. """
  129. 处理库位组和库位的关联关系
  130. :param location_code: 库位编码
  131. :param location_group: 库位组编码
  132. :return:
  133. """
  134. # 1. 获取库位空闲状态的库位数目
  135. location_obj_number = LocationModel.objects.filter(
  136. location_group=location_group,
  137. status='available'
  138. ).all().count()
  139. # 2. 获取库位组对象
  140. logger.info(f"库位组 {location_group} 下的库位数目:{location_obj_number}")
  141. # 1. 获取库位和库位组的关联关系
  142. location_group_obj = LocationGroupModel.objects.filter(
  143. group_code=location_group
  144. ).first()
  145. if not location_group_obj:
  146. logger.info(f"库位组 {location_group} 不存在")
  147. return None
  148. else:
  149. if location_obj_number == 0:
  150. # 库位组库位已满,更新库位组状态为full
  151. location_group_obj.status = 'full'
  152. location_group_obj.save()
  153. elif location_obj_number < location_group_obj.max_capacity:
  154. location_group_obj.status = 'occupied'
  155. location_group_obj.save()
  156. else:
  157. location_group_obj.status = 'available'
  158. location_group_obj.save()
  159. def batch_status_location(self, request):
  160. """
  161. 优化版:批量获取库位批次状态
  162. 基于模型结构优化查询
  163. """
  164. layer = request.data.get('layer')
  165. # 使用反向关系名 'container_links' 进行预取
  166. locations = LocationModel.objects.filter(
  167. layer=layer
  168. ).prefetch_related(
  169. Prefetch(
  170. 'container_links', # 使用模型定义的 related_name
  171. queryset=LocationContainerLink.objects.filter(is_active=True)
  172. .select_related('container'),
  173. to_attr='active_links'
  174. )
  175. )
  176. # 收集所有激活链接的托盘ID
  177. container_ids = set()
  178. for loc in locations:
  179. if loc.active_links: # 每个库位最多只有一个激活链接
  180. container_ids.add(loc.active_links[0].container_id)
  181. # 批量查询托盘详情及其批次状态
  182. container_batch_status = defaultdict(dict) # 改为字典存储,避免重复记录
  183. if container_ids:
  184. container_details = ContainerDetailModel.objects.filter(
  185. container_id__in=container_ids ,
  186. is_delete=False
  187. ).select_related('batch').exclude(status=3) # 排除已删除或不合格的托盘
  188. for detail in container_details:
  189. if detail.batch_id:
  190. # 创建唯一标识的键
  191. status_key = (
  192. detail.batch.check_status if detail.batch else "404",
  193. detail.batch.bound_number if detail.batch else "no_batch"
  194. )
  195. # 如果这个状态尚未添加过,或者需要更新
  196. if status_key not in container_batch_status[detail.container_id]:
  197. container_batch_status[detail.container_id][status_key] = (
  198. detail.batch.check_status if detail.batch else "404",
  199. detail.batch.check_time if detail.batch else "no_check_time",
  200. detail.batch.bound_number if detail.batch else "no_batch",
  201. detail.goods_qty-detail.goods_out_qty if detail.goods_qty-detail.goods_out_qty > 0 else 0,
  202. )
  203. else:
  204. # 如果批次状态相同,则更新库位数量
  205. if container_batch_status[detail.container_id][status_key][0] == detail.batch.check_status:
  206. container_batch_status[detail.container_id][status_key] = (
  207. detail.batch.check_status,
  208. max(container_batch_status[detail.container_id][status_key][1], detail.batch.check_time),
  209. detail.batch.bound_number,
  210. container_batch_status[detail.container_id][status_key][3] + (detail.goods_qty-detail.goods_out_qty if detail.goods_qty-detail.goods_out_qty > 0 else 0)
  211. )
  212. # 构造返回数据
  213. return_data = []
  214. for loc in locations:
  215. batch_statuses = []
  216. if loc.active_links:
  217. container_id = loc.active_links[0].container_id
  218. # 从字典中提取值并转换为列表
  219. if container_id in container_batch_status:
  220. batch_statuses = list(container_batch_status[container_id].values())
  221. else:
  222. batch_statuses = [("404", "no_check_time", "no_batch")]
  223. # 使用Django模型自带的model_to_dict转换基础字段
  224. from django.forms.models import model_to_dict
  225. location_data = model_to_dict(loc, fields=[
  226. "id", "shelf_type", "row", "col", "layer", "update_time",
  227. "empty_label", "location_code", "location_group", "location_type",
  228. "status", "max_capacity", "current_quantity", "c_number",
  229. "coordinate", "access_priority", "is_active"
  230. ])
  231. # 添加批次状态字段 - 存储所有信息
  232. location_data["batch_statuses"] = batch_statuses
  233. return_data.append(location_data)
  234. data = {
  235. "code": "200",
  236. "msg": "Success Create",
  237. "data": return_data
  238. }
  239. log_operation(
  240. request=self.request,
  241. operation_content=f"批量获取库位批次状态,层号:{layer}",
  242. operation_level="view",
  243. operator=self.request.auth.name if self.request.auth else None,
  244. module_name="库位"
  245. )
  246. return Response(data, status=200)
  247. class locationGroupViewSet(viewsets.ModelViewSet):
  248. """
  249. retrieve:
  250. Response a data list(get)
  251. list:
  252. Response a data list(all)
  253. create:
  254. Create a data line(post)
  255. delete:
  256. Delete a data line(delete)
  257. """
  258. # authentication_classes = [] # 禁用所有认证类
  259. # permission_classes = [AllowAny] # 允许任意访问
  260. pagination_class = MyPageNumberPagination
  261. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  262. ordering_fields = ['id', "create_time", "update_time", ]
  263. filter_class = LocationGroupFilter
  264. def get_project(self):
  265. try:
  266. id = self.kwargs.get('pk')
  267. return id
  268. except:
  269. return None
  270. def get_queryset(self):
  271. id = self.get_project()
  272. if self.request.auth:
  273. if id is None:
  274. return LocationGroupModel.objects.filter()
  275. else:
  276. return LocationGroupModel.objects.filter(id=id)
  277. else:
  278. return LocationGroupModel.objects.none()
  279. def get_serializer_class(self):
  280. if self.action == 'list':
  281. return LocationGroupListSerializer
  282. elif self.action == 'update':
  283. return LocationGroupPostSerializer
  284. elif self.action =='retrieve':
  285. return LocationGroupListSerializer
  286. def update(self, request, *args, **kwargs):
  287. data = self.request.data
  288. order_month = str(timezone.now().strftime('%Y%m'))
  289. data['month'] = order_month
  290. group_code = data.get('group_code')
  291. # 处理库位组对象
  292. group_obj = LocationGroupModel.objects.filter(group_code=group_code).first()
  293. if group_obj:
  294. data['id'] = group_obj.id
  295. logger.info(f"库位组 {group_code} 已存在")
  296. else:
  297. logger.info(f"库位组 {group_code} 不存在,创建库位组对象")
  298. serializer_list = LocationGroupPostSerializer(data=data)
  299. serializer_list.is_valid(raise_exception=True)
  300. serializer_list.save()
  301. log_success_operation(
  302. request=self.request,
  303. operation_content=f"创建库位组成功,库位组 {group_code} 已创建",
  304. operation_level="new",
  305. operator=self.request.auth.name if self.request.auth else None,
  306. object_id=serializer_list.data.get('id'),
  307. module_name="库位"
  308. )
  309. data['id'] = serializer_list.data.get('id')
  310. return Response(data, status=status.HTTP_201_CREATED)
  311. class LocationAllocation:
  312. # 入库规则函数
  313. # fun:get_pallet_count_by_batch: 根据托盘码查询批次下托盘总数
  314. # fun:get_left_locationGroup_number_by_type: 获取每层库位组剩余数量
  315. # fun:get_location_type: 根据托盘数目获取库位类型
  316. # fun:update_location_container_link: 更新库位和托盘的关联关系
  317. # fun:update_location_group_batch: 更新库位组的批次
  318. # fun:update_batch_status: 更新批次状态yes/no
  319. # fun:update_location_status: 更新库位状态和
  320. # fun:up
  321. # fun:get_batch_status: 获取批次状态
  322. # fun:get_batch: 获取批次
  323. # fun:get_location_list_remainder: 获取可用库位的c_number列表
  324. # fun
  325. # fun:get_location_by_type_remainder: 根据库位类型获取库位
  326. # fun:get_location_by_type: 第一次入库,根据库位类型获取库位
  327. # fun:get_location_by_status: 根据库位状态获取库位
  328. def get_pallet_count_by_batch(self, container_code):
  329. """
  330. 根据托盘码查询批次下托盘总数
  331. :param container_code: 要查询的托盘码
  332. :return: 所属批次下的托盘总数
  333. """
  334. # 1. 通过托盘码获取托盘详情
  335. container = ContainerListModel.objects.filter(
  336. container_code=container_code
  337. ).first()
  338. if not container:
  339. logger.error(f"托盘 {container_code} 不存在")
  340. return None
  341. # 2. 获取关联的批次明细
  342. container_detail = ContainerDetailModel.objects.filter(
  343. container=container.id,is_delete=False
  344. ).exclude(status = 3).first()
  345. if not container_detail:
  346. logger.error(f"托盘 {container_code} 未组盘")
  347. return None
  348. batch_container = ContainerDetailModel.objects.filter(
  349. batch = container_detail.batch.id,
  350. is_delete = False,
  351. status = 1
  352. ).all()
  353. # 统计批次下的不同托盘 item.contianer_id
  354. batch_container_count = 0
  355. container_ids = []
  356. for item in batch_container:
  357. if item.container_id not in container_ids:
  358. batch_container_count = batch_container_count + 1
  359. container_ids.append(item.container_id)
  360. batch_item = BoundBatchModel.objects.filter( bound_number = container_detail.batch.bound_number).first()
  361. if not batch_item:
  362. print(f"批次号获取失败!")
  363. return None
  364. batch_item.container_number = batch_container_count
  365. batch_item.save()
  366. return batch_container_count
  367. def get_left_locationGroup_number_by_type(self):
  368. """
  369. 获取每层库位组剩余数量
  370. :return:
  371. """
  372. try:
  373. # 定义库位组和层号
  374. group = ['T1', 'T2', 'S4', 'T4', 'T5']
  375. layer = [1, 2, 3]
  376. # 初始化结果列表,包含三个空字典对应三个层
  377. left_number = [{} for _ in layer]
  378. for item in group:
  379. for idx, layer_num in enumerate(layer):
  380. # 检查库位组是否存在(不考虑状态)
  381. exists = LocationGroupModel.objects.filter(
  382. group_type=item,
  383. layer=layer_num
  384. ).exists()
  385. if not exists:
  386. print(f"库位组 {item}_{layer_num} 不存在")
  387. left_number[idx][item] = 0
  388. else:
  389. # 统计可用状态的库位组数量
  390. count = LocationGroupModel.objects.filter(
  391. group_type=item,
  392. layer=layer_num,
  393. status='available'
  394. ).count()
  395. left_number[idx][item] = count
  396. return left_number
  397. except Exception as e:
  398. logger.error(f"获取库位组剩余数量失败:{str(e)}")
  399. print(f"获取库位组剩余数量失败:{str(e)}")
  400. return None
  401. def update_location_container_link(self,location_code,container_code):
  402. """
  403. 更新库位和托盘的关联关系
  404. :param location_code: 库位编码
  405. :param container_code: 托盘编码
  406. :return:
  407. """
  408. try:
  409. # 1. 获取库位和托盘的关联关系
  410. location = LocationModel.objects.filter(
  411. location_code=location_code
  412. ).first()
  413. container = ContainerListModel.objects.filter(
  414. container_code=container_code
  415. ).first()
  416. # 2. 如果库位和托盘的关联关系不存在,创建新的关联关系
  417. if not LocationContainerLink.objects.filter(location=location).exists():
  418. location_container_link = LocationContainerLink(
  419. location=location,
  420. container=container
  421. )
  422. location_container_link.save()
  423. print(f"更新库位和托盘的关联关系成功!")
  424. return True
  425. # 3. 更新库位和托盘的关联关系
  426. else:
  427. LocationContainerLink.objects.filter(location=location).update(location=location, container=container)
  428. print(f"更新库位和托盘的关联关系成功!")
  429. return True
  430. except Exception as e:
  431. logger.error(f"更新库位和托盘的关联关系失败:{str(e)}")
  432. print(f"更新库位和托盘的关联关系失败:{str(e)}")
  433. return False
  434. def update_container_detail_status(self,container_code,status):
  435. try:
  436. # 1. 获取托盘
  437. container = ContainerListModel.objects.filter(
  438. container_code=container_code
  439. ).first()
  440. if not container:
  441. print(f"托盘 {container_code} 不存在")
  442. return False
  443. # 2. 更新托盘状态
  444. container_detail = ContainerDetailModel.objects.filter(
  445. container=container.id,is_delete=False
  446. ).exclude(status=3).first()
  447. if not container_detail:
  448. print(f"托盘 {container_code} 未组盘_from update_container_detail_status")
  449. return False
  450. container_detail.status = status
  451. container_detail.save()
  452. print(f"更新托盘状态成功!")
  453. return True
  454. except Exception as e:
  455. logger.error(f"更新托盘状态失败:{str(e)}")
  456. print(f"更新托盘状态失败:{str(e)}")
  457. return False
  458. def update_location_group_batch(self,location,container_code):
  459. """
  460. :param location: 库位对象
  461. :param container_code: 托盘码
  462. :return:
  463. """
  464. try:
  465. # 1. 获取库位组
  466. location_group = LocationGroupModel.objects.filter(
  467. group_code=location.location_group
  468. ).first()
  469. if not location_group:
  470. print(f"库位组获取失败!")
  471. return False
  472. # 2. 更新库位组的批次
  473. bound_number=self.get_batch(container_code)
  474. if not bound_number:
  475. print(f"批次号获取失败!")
  476. return False
  477. location_group.current_batch = bound_number
  478. location_group.save()
  479. print(f"更新库位组的批次成功!")
  480. return True
  481. except Exception as e:
  482. logger.error(f"更新库位组的批次失败:{str(e)}")
  483. print(f"更新库位组的批次失败:{str(e)}")
  484. return False
  485. def update_location_status(self,location_code,status):
  486. """
  487. 更新库位状态
  488. :param location_code: 库位编码
  489. :param status: 库位状态
  490. :return:
  491. """
  492. try:
  493. # 1. 获取库位
  494. location = LocationModel.objects.filter(
  495. location_code=location_code
  496. ).first()
  497. if not location:
  498. print(f"库位获取失败!")
  499. return False
  500. # 2. 更新库位状态
  501. location.status = status
  502. location.save()
  503. print(f"更新库位状态成功!")
  504. return True
  505. except Exception as e:
  506. logger.error(f"更新库位状态失败:{str(e)}")
  507. print(f"更新库位状态失败:{str(e)}")
  508. return False
  509. def update_group_status_reserved(self,location_group_list):
  510. """
  511. 更新库位组状态
  512. :param location_group_list: 库位组对象列表
  513. :return:
  514. """
  515. try:
  516. for location_group in location_group_list:
  517. # 1. 获取库位组
  518. if not location_group:
  519. print(f"库位组获取失败!")
  520. return False
  521. # 2. 更新库位组状态
  522. location_group_id = location_group.split('_')[1]
  523. location_group_item = LocationGroupModel.objects.filter(
  524. id=location_group_id
  525. ).first()
  526. if not location_group_item:
  527. print(f"库位组 {location_group} 不存在")
  528. return False
  529. # 3. 更新库位组状态
  530. location_group_item.status = 'reserved'
  531. location_group_item.save()
  532. return True
  533. except Exception as e:
  534. logger.error(f"更新库位组状态失败:{str(e)}")
  535. print(f"更新库位组状态失败:{str(e)}")
  536. return False
  537. def update_location_group_status(self, location_code):
  538. """
  539. 更新库位组状态
  540. :param location_code: 库位编码
  541. :return:
  542. """
  543. try:
  544. # 1. 获取库位
  545. location = LocationModel.objects.filter(
  546. location_code=location_code
  547. ).first()
  548. if not location:
  549. print(f"库位获取失败!")
  550. return False
  551. # 2. 获取库位组
  552. location_group = LocationGroupModel.objects.filter(
  553. group_code=location.location_group
  554. ).first()
  555. if not location_group:
  556. print(f"库位组获取失败!")
  557. return False
  558. current=0
  559. for location_item in location_group.location_items.all():
  560. if location_item.status != 'available':
  561. current=current + 1
  562. # 3. 更新库位组状态
  563. if current == 0:
  564. location_group.status = 'available'
  565. elif current == location_group.max_capacity:
  566. location_group.status = 'full'
  567. else:
  568. location_group.status = 'occupied'
  569. location_group.current_goods_quantity = sum(
  570. [loc.current_quantity for loc in location_group.location_items.all()]
  571. )
  572. location_group.current_quantity = current
  573. location_group.save()
  574. print(f"更新库位组状态成功!")
  575. return True
  576. except Exception as e:
  577. logger.error(f"更新库位组状态失败:{str(e)}")
  578. print(f"更新库位组状态失败:{str(e)}")
  579. def update_batch_status(self,container_code,status):
  580. """
  581. 更新批次状态
  582. :param batch_id: 批次id
  583. :param status: 批次状态
  584. :return:
  585. """
  586. try:
  587. # 1. 通过托盘码获取托盘详情
  588. container = ContainerListModel.objects.filter(
  589. container_code=container_code
  590. ).first()
  591. if not container:
  592. logger.error(f"托盘 {container_code} 不存在")
  593. print(f"托盘 {container_code} 不存在")
  594. return None
  595. # 2. 获取关联的批次明细
  596. container_detail = ContainerDetailModel.objects.filter(
  597. container=container.id,is_delete=False
  598. ).exclude(status=3).first()
  599. if not container_detail:
  600. print (f"托盘 {container_code} 未组盘")
  601. logger.error(f"托盘 {container_code} 未组盘_from update_batch_status")
  602. return None
  603. # 3. 更新批次状态
  604. batch = container_detail.batch
  605. batch.status = status
  606. batch.save()
  607. print(f"更新批次状态成功!")
  608. return True
  609. except Exception as e:
  610. logger.error(f"更新批次状态失败:{str(e)}")
  611. print(f"更新批次状态失败:{str(e)}")
  612. return False
  613. # def update_batch_goods_in_location_qty(self,container_code,taskworking):
  614. # """
  615. # 更新批次库位入库数量
  616. # :param container_code: 托盘码
  617. # :param goods_in_location_qty: 库位入库数量
  618. # :return:
  619. # """
  620. # try:
  621. # # 1. 通过托盘码获取托盘详情
  622. # container = ContainerListModel.objects.filter(
  623. # container_code=container_code
  624. # ).first()
  625. # if not container:
  626. # logger.error(f"托盘 {container_code} 不存在")
  627. # print(f"托盘 {container_code} 不存在")
  628. # return None
  629. # # 2. 获取关联的批次明细
  630. # container_detail = ContainerDetailModel.objects.filter(
  631. # container=container.id,is_delete=False
  632. # ).exclude(status=3).all()
  633. # if not container_detail:
  634. # print (f"托盘 {container_code} 未组盘")
  635. # logger.error(f"托盘 {container_code} 未组盘_from update_batch_goods_in_location_qty")
  636. # return None
  637. # for item in container_detail:
  638. # if item.goods_class == 2:
  639. # continue
  640. # item.batch.goods_in_location_qty += item.goods_qty * taskworking
  641. # item.batch.save()
  642. # print(f"更新批次库位入库数量成功!")
  643. # return True
  644. # except Exception as e:
  645. # logger.error(f"更新批次库位入库数量失败:{str(e)}")
  646. # print(f"更新批次库位入库数量失败:{str(e)}")
  647. # return False
  648. def get_batch_status(self,container_code):
  649. """
  650. 获取批次状态
  651. :param container_code: 托盘码
  652. :return: 批次状态
  653. """
  654. # 1. 通过托盘码获取托盘详情
  655. container = ContainerListModel.objects.filter(
  656. container_code=container_code
  657. ).first()
  658. if not container:
  659. logger.error(f"托盘 {container_code} 不存在")
  660. print(f"托盘 {container_code} 不存在")
  661. return None
  662. # 2. 获取关联的批次明细
  663. container_detail = ContainerDetailModel.objects.filter(
  664. container=container.id,is_delete=False
  665. ).exclude(status=3).first()
  666. if not container_detail:
  667. print (f"托盘 {container_code} 未组盘")
  668. logger.error(f"托盘 {container_code} 未组盘_from get_batch_status")
  669. return None
  670. batch_status = container_detail.batch.status
  671. return batch_status
  672. def get_batch(self,container_code):
  673. """
  674. 获取批次
  675. :param container_code: 托盘码
  676. :return: 批次
  677. """
  678. # 1. 通过托盘码获取托盘详情
  679. container = ContainerListModel.objects.filter(
  680. container_code=container_code
  681. ).first()
  682. if not container:
  683. logger.error(f"托盘 {container_code} 不存在")
  684. print(f"托盘 {container_code} 不存在")
  685. return None
  686. # 2. 获取关联的批次明细
  687. container_detail = ContainerDetailModel.objects.filter(
  688. container=container.id,is_delete=False
  689. ).exclude(status=3).first()
  690. if not container_detail:
  691. print (f"托盘 {container_code} 未组盘")
  692. logger.error(f"托盘 {container_code} 未组盘_from get_batch")
  693. return None
  694. batch = container_detail.batch.bound_number
  695. return batch
  696. def get_location_list_remainder(self, location_group_list,container_code):
  697. """
  698. 获取可用库位的c_number列表
  699. :param location_list: 库位对象列表
  700. :return: 可用库位编号列表
  701. """
  702. if not location_group_list:
  703. return None
  704. min_c_number=1000
  705. min_c_number_index=1000
  706. current_task = self.get_current_finish_task(container_code)
  707. print(f"[1]当前已完成任务: {current_task}")
  708. # 按压力排序
  709. sorted_pressure = sorted(
  710. [(0, current_task[0]), (1, current_task[1]), (2, current_task[2])],
  711. key=lambda x: (-x[1], x[0])
  712. )
  713. # 交换第一和第二个元素的位置
  714. sorted_pressure[0], sorted_pressure[1] = sorted_pressure[1], sorted_pressure[0]
  715. print(f"[2]任务排序: {sorted_pressure}")
  716. print(f"[3]当前选择:{sorted_pressure[0][0]+1}")
  717. location_type_dict = json.loads(self.divide_solution_by_layer(location_group_list))
  718. # print(f"库位类型分配方案: {location_type_dict}")
  719. for layer, _ in sorted_pressure:
  720. if not location_type_dict.get(str(layer+1)):
  721. continue
  722. # print(f"当前层: {layer+1}")
  723. # print(f"当前层库位组: {location_type_dict[str(layer+1)].keys()}")
  724. for group_id in location_type_dict[str(layer+1)].keys():
  725. location_group = LocationGroupModel.objects.filter(
  726. id=group_id,
  727. ).first()
  728. if not location_group:
  729. continue
  730. location_list = location_group.location_items.filter(
  731. status='available'
  732. ).all().order_by('c_number')
  733. if not location_list:
  734. print(f"当前层库位组 {location_group.group_code} 可用库位: None")
  735. continue
  736. # 提取所有库位的 c_number
  737. c_numbers = [loc.c_number for loc in location_list]
  738. print(f"当前层库位组 {location_group.group_code} 可用库位: {c_numbers}")
  739. # 更新任务完成数目
  740. current_task[layer] = current_task[layer] + 1
  741. self.update_current_finish_task(container_code,current_task)
  742. return location_list[0]
  743. def get_location_type(self, container_code):
  744. """
  745. 智能库位分配核心算法
  746. :param container_code: 托盘码
  747. :return: 库位类型分配方案
  748. """
  749. try:
  750. batch = self.get_batch(container_code)
  751. if not batch:
  752. logger.error("批次信息获取失败")
  753. return None
  754. # 检查已有分配方案
  755. existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
  756. if existing_solution:
  757. return existing_solution.layer_pre_type
  758. # 获取关键参数
  759. total_pallets = self.get_pallet_count_by_batch(container_code)
  760. layer_capacity = self.get_left_locationGroup_number_by_type()
  761. current_pressure = self.get_current_pressure()
  762. # 测试参数
  763. # total_pallets = 30
  764. # layer_capacity = [{'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}, {'T1': 0, 'T2': 0, 'S4': 0, 'T4': 0, 'T5': 21}, {'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}]
  765. # current_pressure = [1,0,0]
  766. print(f"[1]托盘数目: {total_pallets}")
  767. print(f"[2]层容量: {layer_capacity}")
  768. # print(f"[3]当前压力: {current_pressure}")
  769. # 定义库位容量表
  770. LOCATION_CAPACITY = {'T1':1, 'T2':2, 'T4':4, 'S4':4, 'T5':5}
  771. def allocate(remain, path, pressure,real_pressure,layer_capacity_state, depth=0):
  772. # 终止条件
  773. if remain <= 0:
  774. return [path,real_pressure]
  775. # 深拷贝当前层容量状态
  776. new_layer_capacity = copy.deepcopy(layer_capacity_state)
  777. # print(f"[2]当前剩余: {new_layer_capacity}")
  778. # 压力平衡系数
  779. balance_factor = 1.0 - (0.1 * min(depth, 5))
  780. # 层选择策略
  781. print (f"[3]当前压力: {pressure}")
  782. layer_priority = sorted(
  783. [(0, pressure[0]), (1, pressure[1]), (2, pressure[2])],
  784. key=lambda x: (x[1] * balance_factor, x[0])
  785. )
  786. for layer, _ in layer_priority:
  787. # 生成候选库位类型(按效率和容量排序)
  788. # 排序键函数 :
  789. # min(x[1], remain) 计算当前库位类型的容量 c 和剩余数量 remain 中的较小值。
  790. # -min(x[1], remain) 和 -x[1] 都使用了负号,这意味着排序是按降序进行的。
  791. # 首先按 -min(x[1], remain) 排序,即优先选择容量与剩余数量更接近的库位类型。
  792. # 如果有多个库位类型的容量与剩余数量相同,则按 -x[1] 排序,即优先选择容量更大的库位类型。
  793. print(f"[4]当前层: {layer+1}, 剩余: {remain}, 容量状态: {new_layer_capacity[layer]}")
  794. candidates = sorted(
  795. [(t, c) for t, c in LOCATION_CAPACITY.items()
  796. if new_layer_capacity[layer].get(t,0) > 0],
  797. key=lambda x: (abs(x[1]-remain), -x[1])
  798. )
  799. print(f"[4]候选库位类型: {candidates}")
  800. for loc_type, cap in candidates:
  801. # 更新容量状态
  802. updated_capacity = copy.deepcopy(new_layer_capacity)
  803. updated_capacity[layer][loc_type] -= 1 # 占用一个库位组
  804. # 允许适度空间浪费(当剩余<2时)
  805. # effective_cap = min(cap, remain) if (cap - remain) < 2 else cap
  806. effective_cap = min(cap, remain)
  807. if effective_cap <= remain:
  808. new_remain = remain - effective_cap
  809. new_pressure = pressure.copy()
  810. for i in range(0, 3):
  811. new_pressure[i] -=1 if new_pressure[i] > 0 else 0
  812. new_pressure[layer] += effective_cap # 按实际存放数计算压力,此时别的楼层压力可能降下来了
  813. real_pressure[layer] += effective_cap # 实际压力
  814. result = allocate(
  815. new_remain,
  816. path + [f"{layer+1}_{loc_type}"],
  817. new_pressure,
  818. real_pressure,
  819. updated_capacity,
  820. depth + 1
  821. )
  822. if result:
  823. print (f"[5]分配方案: {result}")
  824. return result
  825. return None
  826. # 执行分配
  827. allocation = allocate(total_pallets, [], [current_pressure[0], current_pressure[1],current_pressure[2]],[current_pressure[0], current_pressure[1],current_pressure[2]], layer_capacity)
  828. if not allocation:
  829. logger.error("无法生成有效分配方案")
  830. return None
  831. # 保存分配方案
  832. allocation_json = self.divide_solution_by_layer(allocation[0])
  833. print(f"[6]分配方案: {allocation_json}")
  834. solution = alloction_pre(
  835. batch_number=batch,
  836. layer_pre_type =allocation_json
  837. )
  838. solution_pressure, created = base_location.objects.get_or_create(
  839. id=1,
  840. defaults={
  841. 'layer1_pressure': 0,
  842. 'layer2_pressure': 0,
  843. 'layer3_pressure': 0
  844. }
  845. )
  846. solution_pressure.layer1_pressure = allocation[1][0]
  847. solution_pressure.layer2_pressure = allocation[1][1]
  848. solution_pressure.layer3_pressure = allocation[1][2]
  849. solution.save()
  850. solution_pressure.save()
  851. return [loc.split('_')[1] for loc in allocation[0]]
  852. except Exception as e:
  853. logger.error(f"分配算法异常:{str(e)}")
  854. return None
  855. def divide_solution_by_layer(self, data):
  856. # 统计所有存在的层级
  857. layer_counts = defaultdict(lambda: defaultdict(int))
  858. existing_layers = set()
  859. for item in data:
  860. # 分割层级和类型
  861. try:
  862. layer, loc_type = item.split('_')
  863. layer_num = int(layer)
  864. existing_layers.add(layer_num)
  865. layer_counts[layer_num][loc_type] += 1
  866. except (ValueError, IndexError):
  867. continue # 跳过无效格式的数据
  868. # 确定最大层级(至少包含1层)
  869. max_layer = max(existing_layers) if existing_layers else 1
  870. # 构建包含所有层级的最终结果
  871. final_result = {}
  872. for layer in range(1, max_layer + 1):
  873. final_result[str(layer)] = dict(layer_counts.get(layer, {}))
  874. return json.dumps(final_result, indent=2)
  875. def get_current_pressure(self):
  876. """获取实时工作压力"""
  877. last_solution = base_location.objects.order_by('-id').first()
  878. if not last_solution:
  879. base_location.objects.create(
  880. layer1_pressure=0,
  881. layer2_pressure=0,
  882. layer3_pressure=0,
  883. ).save()
  884. return [
  885. last_solution.layer1_pressure if last_solution else 0,
  886. last_solution.layer2_pressure if last_solution else 0,
  887. last_solution.layer3_pressure if last_solution else 0,
  888. ]
  889. def get_current_finish_task(self,container):
  890. batch = self.get_batch(container)
  891. if not batch:
  892. return None
  893. solution = alloction_pre.objects.filter(batch_number=batch).first()
  894. if not solution:
  895. return None
  896. return [solution.layer1_task_finish_number,solution.layer2_task_finish_number,solution.layer3_task_finish_number]
  897. def update_current_finish_task(self,container,task_finish_number):
  898. batch = self.get_batch(container)
  899. if not batch:
  900. return None
  901. solution = alloction_pre.objects.filter(batch_number=batch).first()
  902. if not solution:
  903. return None
  904. solution.layer1_task_finish_number = task_finish_number[0]
  905. solution.layer2_task_finish_number = task_finish_number[1]
  906. solution.layer3_task_finish_number = task_finish_number[2]
  907. solution.save()
  908. return True
  909. def get_location_by_type(self, location_type_list, start_location, container_code):
  910. """
  911. 根据库位类型获取库位,先根据工作压力找出最空闲的层,看下这层有没有工作,如果有,就从里面找,如果没有,则跳过该层,继续找下一层
  912. :param location_type_list: 库位分配方案
  913. {
  914. "1": {
  915. "S4": 1
  916. },
  917. "2": {},
  918. "3": {
  919. "T5": 1
  920. }
  921. }
  922. :param start_location: 起始位置(决定优先级排序方式)
  923. :return: 符合条件的库位列表
  924. """
  925. locations = []
  926. # 检查已有分配方案
  927. existing_solution = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first()
  928. if existing_solution.layer_solution_type:
  929. print(f"[0]已有库位分配方案:{existing_solution.layer_solution_type}")
  930. return existing_solution.layer_solution_type
  931. for layer, location_type_dict in location_type_list.items():
  932. if not location_type_dict:
  933. continue
  934. # 获取库位类型列表
  935. location_type = list(location_type_dict.keys())
  936. demand_number = sum(location_type_dict.values())
  937. print (f"[1]层{layer} 需求数量: {demand_number}, 库位: {location_type}")
  938. location_groups = LocationGroupModel.objects.filter(
  939. group_type__in=location_type,
  940. layer=layer,
  941. status='available'
  942. )
  943. if not location_groups:
  944. print(f"层{layer} 无库位")
  945. # 根据起始位置选择排序字段
  946. if start_location == '203':
  947. ordered_groups = location_groups.order_by('left_priority')
  948. elif start_location == '103':
  949. ordered_groups = location_groups.order_by('right_priority')
  950. else:
  951. ordered_groups = location_groups.none()
  952. number = 0
  953. for location_group in ordered_groups:
  954. if number >= demand_number:
  955. break
  956. locations.append(f"{layer}_{location_group.id}")
  957. number += 1
  958. existing_solution.layer_solution_type = locations
  959. existing_solution.save()
  960. print(f"[2]分配方案: {locations}")
  961. return locations if locations else None
  962. def get_location_by_status(self,container_code,start_location):
  963. """
  964. 根据库位状态获取库位
  965. :param location_type: 库位类型
  966. :param start_location: 起始库位 if in1 优先考虑left_priority, if in2 优先考虑right_priority 就是获取库位组列表之后进行排序
  967. :return: 库位列表
  968. """
  969. # 1. 获取批次状态 1 为已组盘 2 为部分入库 3 为全部入库
  970. status = self.get_batch_status(container_code)
  971. #
  972. if status == 1:
  973. # 2. 获取库位组
  974. print(f"[1]第一次入库")
  975. # 重新获取最新数据
  976. self.get_location_type(container_code)
  977. location_type_list = json.loads(alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_pre_type)
  978. location_list = self.get_location_by_type(location_type_list,start_location,container_code)
  979. # 预定这些库组
  980. self.update_group_status_reserved(location_list)
  981. location_min_value = self.get_location_list_remainder(location_list,container_code)
  982. print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
  983. # if not location_list[location_min_index]:
  984. # # 库位已满,返回None
  985. # return None
  986. # else:
  987. # return location_list[location_min_index]
  988. return location_min_value
  989. elif status == 2:
  990. # 3. 获取部分入库库位
  991. print (f"部分入库")
  992. location_list = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_solution_type
  993. location_min_value = self.get_location_list_remainder(location_list,container_code)
  994. print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
  995. return location_min_value
  996. def release_location(self, location_code):
  997. """释放库位并更新关联数据"""
  998. try:
  999. location = LocationModel.objects.get(location_code=location_code)
  1000. links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
  1001. if not links:
  1002. logger.error(f"库位{location_code}未关联托盘")
  1003. return True
  1004. print(f"释放库位: {location_code}, 关联托盘: {links.container_id}")
  1005. # 解除关联并标记为非活跃
  1006. links.is_active = False
  1007. links.save()
  1008. return True
  1009. except Exception as e:
  1010. logger.error(f"释放库位失败: {str(e)}")
  1011. return False