views.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679
  1. from wsgiref import headers
  2. from rest_framework.views import APIView
  3. from rest_framework import viewsets
  4. from utils.page import MyPageNumberPagination
  5. from django.db.models import Prefetch
  6. from rest_framework.filters import OrderingFilter
  7. from django_filters.rest_framework import DjangoFilterBackend
  8. from rest_framework.response import Response
  9. from django.db.models import F, Case, When
  10. from django.utils import timezone
  11. import requests
  12. from django.db import transaction
  13. import logging
  14. from rest_framework import status
  15. from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,ContainerWCSModel,TaskModel
  16. from bound.models import BoundDetailModel,BoundListModel,OutBoundDetailModel
  17. from bin.views import LocationAllocation,base_location
  18. from bin.models import LocationModel,LocationContainerLink,LocationGroupModel
  19. from bound.models import BoundBatchModel,OutBatchModel,BatchLogModel
  20. from .serializers import ContainerDetailGetSerializer,ContainerDetailPostSerializer,ContainerDetailSimpleGetSerializer
  21. from .serializers import ContainerListGetSerializer,ContainerListPostSerializer
  22. from .serializers import ContainerOperationGetSerializer,ContainerOperationPostSerializer
  23. from .serializers import TaskGetSerializer,TaskPostSerializer
  24. from .serializers import WCSTaskGetSerializer
  25. from .filter import ContainerDetailFilter,ContainerListFilter,ContainerOperationFilter,TaskFilter,WCSTaskFilter
  26. from rest_framework.permissions import AllowAny
  27. import threading
  28. from django.db import close_old_connections
  29. from bin.services import AllocationService
  30. from collections import defaultdict
  31. logger = logging.getLogger(__name__)
  32. # 托盘列表视图
  33. class ContainerListViewSet(viewsets.ModelViewSet):
  34. """
  35. retrieve:
  36. Response a data list(get)
  37. list:
  38. Response a data list(all)
  39. create:
  40. Create a data line(post)
  41. delete:
  42. Delete a data line(delete)
  43. """
  44. # authentication_classes = [] # 禁用所有认证类
  45. # permission_classes = [AllowAny] # 允许任意访问
  46. pagination_class = MyPageNumberPagination
  47. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  48. ordering_fields = ['id', "create_time", "update_time", ]
  49. filter_class = ContainerListFilter
  50. def get_project(self):
  51. try:
  52. id = self.kwargs.get('pk')
  53. return id
  54. except:
  55. return None
  56. def get_queryset(self):
  57. id = self.get_project()
  58. if self.request.user:
  59. if id is None:
  60. return ContainerListModel.objects.filter()
  61. else:
  62. return ContainerListModel.objects.filter(id=id)
  63. else:
  64. return ContainerListModel.objects.none()
  65. def get_serializer_class(self):
  66. if self.action in ['list', 'destroy','retrieve']:
  67. return ContainerListGetSerializer
  68. elif self.action in ['create', 'update']:
  69. return ContainerListPostSerializer
  70. else:
  71. return self.http_method_not_allowed(request=self.request)
  72. def create(self, request, *args, **kwargs):
  73. # 创建托盘:托盘码五位数字(唯一),当前库位,目标库位,状态,最后操作时间
  74. container_all = ContainerListModel.objects.all().order_by('container_code')
  75. if container_all.count() == 0:
  76. container_code = 12345
  77. else:
  78. container_code = container_all.last().container_code + 1
  79. container_obj = ContainerListModel.objects.create(
  80. container_code=container_code,
  81. current_location='N/A',
  82. target_location='N/A',
  83. status=0,
  84. last_operation=timezone.now()
  85. )
  86. serializer = ContainerListGetSerializer(container_obj)
  87. headers = self.get_success_headers(serializer.data)
  88. return Response(serializer.data, status=201, headers=headers)
  89. def update(self, request, pk):
  90. qs = self.get_object()
  91. data = self.request.data
  92. serializer = self.get_serializer(qs, data=data)
  93. serializer.is_valid(raise_exception=True)
  94. serializer.save()
  95. headers = self.get_success_headers(serializer.data)
  96. return Response(serializer.data, status=200, headers=headers)
  97. # wcs任务视图
  98. class WCSTaskViewSet(viewsets.ModelViewSet):
  99. """
  100. retrieve:
  101. Response a data list(get)
  102. list:
  103. Response a data list(all)
  104. create:
  105. Create a data line(post)
  106. delete:
  107. Delete a data line(delete)
  108. """
  109. # authentication_classes = [] # 禁用所有认证类
  110. # permission_classes = [AllowAny] # 允许任意访问
  111. pagination_class = MyPageNumberPagination
  112. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  113. ordering_fields = ['-id', "-create_time", "update_time", ]
  114. filter_class = WCSTaskFilter
  115. def get_project(self):
  116. try:
  117. id = self.kwargs.get('pk')
  118. return id
  119. except:
  120. return None
  121. def get_queryset(self):
  122. id = self.get_project()
  123. if self.request.user:
  124. if id is None:
  125. return ContainerWCSModel.objects.filter()
  126. else:
  127. return ContainerWCSModel.objects.filter(id=id)
  128. else:
  129. return ContainerWCSModel.objects.none()
  130. def get_serializer_class(self):
  131. if self.action in ['list', 'destroy','retrieve']:
  132. return WCSTaskGetSerializer
  133. else:
  134. return self.http_method_not_allowed(request=self.request)
  135. # 入库任务视图
  136. class TaskViewSet(viewsets.ModelViewSet):
  137. """
  138. retrieve:
  139. Response a data list(get)
  140. list:
  141. Response a data list(all)
  142. create:
  143. Create a data line(post)
  144. delete:
  145. Delete a data line(delete)
  146. """
  147. pagination_class = MyPageNumberPagination
  148. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  149. ordering_fields = ['id', "create_time", "update_time", ]
  150. filter_class = TaskFilter
  151. def get_project(self):
  152. try:
  153. id = self.kwargs.get('pk')
  154. return id
  155. except:
  156. return None
  157. def get_queryset(self):
  158. id = self.get_project()
  159. if self.request.user:
  160. if id is None:
  161. return TaskModel.objects.filter()
  162. else:
  163. return TaskModel.objects.filter(id=id)
  164. else:
  165. return TaskModel.objects.none()
  166. def get_serializer_class(self):
  167. if self.action in ['list', 'destroy','retrieve']:
  168. return TaskGetSerializer
  169. elif self.action in ['create', 'update']:
  170. return TaskPostSerializer
  171. else:
  172. return self.http_method_not_allowed(request=self.request)
  173. def create(self, request, *args, **kwargs):
  174. data = self.request.data
  175. return Response(data, status=200, headers=headers)
  176. def update(self, request, pk):
  177. qs = self.get_object()
  178. data = self.request.data
  179. serializer = self.get_serializer(qs, data=data)
  180. serializer.is_valid(raise_exception=True)
  181. serializer.save()
  182. headers = self.get_success_headers(serializer.data)
  183. return Response(serializer.data, status=200, headers=headers)
  184. # 任务回滚
  185. class TaskRollbackMixin:
  186. @transaction.atomic
  187. def rollback_task(self, request, task_id, *args, **kwargs):
  188. """
  189. 撤销入库任务并回滚相关状态
  190. """
  191. try:
  192. # 获取任务实例并锁定数据库记录
  193. task = ContainerWCSModel.objects.select_for_update().get(taskid=task_id)
  194. container_code = task.container
  195. target_location = task.target_location
  196. batch = task.batch
  197. # 初始化库位分配器
  198. allocator = LocationAllocation()
  199. # ==================== 库位状态回滚 ====================
  200. # 解析目标库位信息(格式:仓库代码-行-列-层)
  201. try:
  202. warehouse_code, row, col, layer = target_location.split('-')
  203. location = LocationModel.objects.get(
  204. warehouse_code=warehouse_code,
  205. row=int(row),
  206. col=int(col),
  207. layer=int(layer)
  208. )
  209. # 回滚库位状态到可用状态
  210. allocator.update_location_status(location.location_code, 'available')
  211. # 更新库位组状态(需要根据实际逻辑实现)
  212. allocator.update_location_group_status(location.location_code)
  213. # 解除库位与托盘的关联
  214. allocator.update_location_container_link(location.location_code, None)
  215. # 清除库位组的批次关联
  216. allocator.update_location_group_batch(location, None)
  217. except (ValueError, LocationModel.DoesNotExist) as e:
  218. logger.error(f"库位解析失败: {str(e)}")
  219. raise Exception("关联库位信息无效")
  220. # ==================== 批次状态回滚 ====================
  221. if batch:
  222. # 将批次状态恢复为未处理状态(假设原状态为1)
  223. allocator.update_batch_status(batch.bound_number, '1')
  224. # ==================== 容器状态回滚 ====================
  225. container_obj = ContainerListModel.objects.get(container_code=container_code)
  226. # 恢复容器详细状态为初始状态(假设原状态为1)
  227. allocator.update_container_detail_status(container_code, 1)
  228. # 恢复容器的目标位置为当前所在位置
  229. container_obj.target_location = task.current_location
  230. container_obj.save()
  231. # ==================== 删除任务记录 ====================
  232. task.delete()
  233. # ==================== 其他关联清理 ====================
  234. # 如果有其他关联数据(如inport_update_task的操作),在此处添加清理逻辑
  235. return Response(
  236. {'code': '200', 'message': '任务回滚成功', 'data': None},
  237. status=status.HTTP_200_OK
  238. )
  239. except ContainerWCSModel.DoesNotExist:
  240. logger.warning(f"任务不存在: {task_id}")
  241. return Response(
  242. {'code': '404', 'message': '任务不存在', 'data': None},
  243. status=status.HTTP_404_NOT_FOUND
  244. )
  245. except Exception as e:
  246. logger.error(f"任务回滚失败: {str(e)}", exc_info=True)
  247. return Response(
  248. {'code': '500', 'message': '服务器内部错误', 'data': None},
  249. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  250. )
  251. # 入库任务下发
  252. class ContainerWCSViewSet(viewsets.ModelViewSet):
  253. """
  254. retrieve:
  255. Response a data list(get)
  256. list:
  257. Response a data list(all)
  258. create:
  259. Create a data line(post)
  260. delete:
  261. Delete a data line(delete)
  262. """
  263. authentication_classes = [] # 禁用所有认证类
  264. permission_classes = [AllowAny] # 允许任意访问
  265. def get_container_wcs(self, request, *args, **kwargs):
  266. data = self.request.data
  267. container = data.get('container_number')
  268. current_location = data.get('current_location')
  269. logger.info(f"请求托盘:{container},请求位置:{current_location}")
  270. data_return = {}
  271. try:
  272. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  273. if not container_obj:
  274. data_return = {
  275. 'code': '400',
  276. 'message': '托盘编码不存在',
  277. 'data': data
  278. }
  279. return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
  280. # 更新容器数据(部分更新)
  281. serializer = ContainerListPostSerializer(
  282. container_obj,
  283. data=data,
  284. partial=True # 允许部分字段更新
  285. )
  286. serializer.is_valid(raise_exception=True)
  287. serializer.save()
  288. # 检查是否已在目标位置
  289. if current_location == str(container_obj.target_location) and current_location!= '203' and current_location!= '103':
  290. logger.info(f"托盘 {container} 已在目标位置")
  291. data_return = {
  292. 'code': '200',
  293. 'message': '当前位置已是目标位置',
  294. 'data': data
  295. }
  296. else:
  297. current_task = ContainerWCSModel.objects.filter(
  298. container=container,
  299. tasktype='inbound',
  300. working = 1,
  301. ).exclude(status=300).first()
  302. if current_task:
  303. data_return = {
  304. 'code': '200',
  305. 'message': '任务已存在,重新下发',
  306. 'data': current_task.to_dict()
  307. }
  308. else:
  309. # todo: 这里的入库操作记录里面的记录的数量不对
  310. location_min_value,allocation_target_location, batch_info = AllocationService.allocate(container, current_location)
  311. batch_id = batch_info['number']
  312. if batch_info['class'] == 2:
  313. self.generate_task_no_batch(container, current_location, allocation_target_location,batch_id,location_min_value.c_number)
  314. self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
  315. elif batch_info['class'] == 3:
  316. self.generate_task_no_batch(container, current_location, allocation_target_location,batch_id,location_min_value.c_number)
  317. self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
  318. else:
  319. self.generate_task(container, current_location, allocation_target_location,batch_id,location_min_value.c_number) # 生成任务
  320. self.generate_container_operate(container_obj, batch_id, allocation_target_location)
  321. current_task = ContainerWCSModel.objects.get(
  322. container=container,
  323. tasktype='inbound',
  324. working=1,
  325. )
  326. data_return = {
  327. 'code': '200',
  328. 'message': '任务下发成功',
  329. 'data': current_task.to_dict()
  330. }
  331. container_obj.target_location = allocation_target_location
  332. container_obj.save()
  333. if batch_info['class'] == 1 or batch_info['class'] == 3:
  334. self.inport_update_task(current_task.id, container_obj.id)
  335. http_status = status.HTTP_200_OK if data_return['code'] == '200' else status.HTTP_400_BAD_REQUEST
  336. return Response(data_return, status=http_status)
  337. except Exception as e:
  338. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  339. return Response(
  340. {'code': '500', 'message': '服务器内部错误', 'data': None},
  341. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  342. )
  343. @transaction.atomic
  344. def generate_container_operate(self, container_obj, bound_number,allocation_target_location):
  345. batch_obj = BoundBatchModel.objects.filter(bound_number=bound_number).first()
  346. ContainerOperationModel.objects.create(
  347. month = int(timezone.now().strftime("%Y%m")),
  348. container = container_obj,
  349. goods_code = batch_obj.goods_code,
  350. goods_desc = batch_obj.goods_desc,
  351. operation_type ="inbound",
  352. batch_id = batch_obj.id,
  353. goods_qty = batch_obj.goods_qty,
  354. goods_weight = batch_obj.goods_qty,
  355. from_location = container_obj.current_location,
  356. to_location= allocation_target_location,
  357. timestamp=timezone.now(),
  358. operator="WMS",
  359. memo=f"WCS入库: 批次: {bound_number}, 数量: {batch_obj.goods_qty}"
  360. )
  361. @transaction.atomic
  362. def generate_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location):
  363. ContainerOperationModel.objects.create(
  364. month = int(timezone.now().strftime("%Y%m")),
  365. container = container_obj,
  366. goods_code = 'container',
  367. goods_desc = '托盘组',
  368. operation_type ="inbound",
  369. goods_qty = 1,
  370. goods_weight = 0,
  371. from_location = container_obj.current_location,
  372. to_location= allocation_target_location,
  373. timestamp=timezone.now(),
  374. memo=f"WCS入库: 批次: {bound_number}, 数量: 1"
  375. )
  376. @transaction.atomic
  377. def generate_task(self, container, current_location, target_location,batch_id,location_c_number):
  378. batch = BoundBatchModel.objects.filter(bound_number=batch_id).first()
  379. batch_detail = BoundDetailModel.objects.filter(bound_batch=batch).first()
  380. if not batch:
  381. logger.error(f"批次号 {batch_id} 不存在")
  382. return False
  383. data_tosave = {
  384. 'container': container,
  385. 'batch': batch,
  386. 'batch_number': batch_id,
  387. 'batch_out': None,
  388. 'bound_list': batch_detail.bound_list,
  389. 'sequence': 1,
  390. 'order_number' :location_c_number,
  391. 'priority': 1,
  392. 'current_location': current_location,
  393. 'month': timezone.now().strftime('%Y%m'),
  394. 'target_location': target_location,
  395. 'tasktype': 'inbound',
  396. 'status': 103,
  397. 'is_delete': False
  398. }
  399. # 生成唯一递增的 taskid
  400. last_task = ContainerWCSModel.objects.filter(
  401. month=data_tosave['month'],
  402. ).order_by('-tasknumber').first()
  403. if last_task:
  404. number_id = last_task.tasknumber + 1
  405. new_id = f"{number_id:05d}"
  406. else:
  407. new_id = "00001"
  408. number_id = f"{data_tosave['month']}{new_id}"
  409. data_tosave['taskid'] = f"inbound-{data_tosave['month']}-{new_id}"
  410. logger.info(f"生成入库任务: {data_tosave['taskid']}")
  411. # 每月生成唯一递增的 taskNumber
  412. data_tosave['tasknumber'] = number_id
  413. ContainerWCSModel.objects.create(**data_tosave)
  414. def generate_task_no_batch(self, container, current_location, target_location,batch_id,location_c_number):
  415. data_tosave = {
  416. 'container': container,
  417. 'batch': None,
  418. 'batch_number': batch_id,
  419. 'batch_out': None,
  420. 'bound_list': None,
  421. 'sequence': 1,
  422. 'order_number' :location_c_number,
  423. 'priority': 1,
  424. 'current_location': current_location,
  425. 'month': timezone.now().strftime('%Y%m'),
  426. 'target_location': target_location,
  427. 'tasktype': 'inbound',
  428. 'status': 103,
  429. 'is_delete': False
  430. }
  431. # 生成唯一递增的 taskid
  432. last_task = ContainerWCSModel.objects.filter(
  433. month=data_tosave['month'],
  434. ).order_by('-tasknumber').first()
  435. if last_task:
  436. number_id = last_task.tasknumber + 1
  437. new_id = f"{number_id:05d}"
  438. else:
  439. new_id = "00001"
  440. number_id = f"{data_tosave['month']}{new_id}"
  441. data_tosave['taskid'] = f"inbound-{data_tosave['month']}-{new_id}"
  442. logger.info(f"生成入库任务: {data_tosave['taskid']}")
  443. # 每月生成唯一递增的 taskNumber
  444. data_tosave['tasknumber'] = number_id
  445. ContainerWCSModel.objects.create(**data_tosave)
  446. def update_container_wcs(self, request, *args, **kwargs):
  447. data = self.request.data
  448. logger.info(f"请求托盘:{data.get('container_number')}, 请求位置:{data.get('current_location')}, 任务号:{data.get('taskNumber')}")
  449. try:
  450. # 前置校验
  451. container_obj, error_response = self.validate_container(data)
  452. if error_response:
  453. return error_response
  454. # 更新容器数据
  455. if not self.update_container_data(container_obj, data):
  456. return Response(
  457. {'code': '400', 'message': '数据更新失败', 'data': data},
  458. status=status.HTTP_400_BAD_REQUEST
  459. )
  460. # 处理位置逻辑
  461. task = ContainerWCSModel.objects.filter(
  462. container=container_obj.container_code,
  463. tasktype='inbound'
  464. ).first()
  465. if self.is_already_at_target(container_obj, data.get('current_location')):
  466. return self.handle_target_reached(container_obj, data)
  467. elif task:
  468. data_return = {
  469. 'code': '200',
  470. 'message': '任务已存在,重新下发',
  471. 'data': task.to_dict()
  472. }
  473. return Response(data_return, status=status.HTTP_200_OK)
  474. else:
  475. return self.handle_new_allocation(container_obj, data)
  476. except Exception as e:
  477. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  478. return Response({'code': '500', 'message': '服务器内部错误', 'data': None},
  479. status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  480. # ---------- 辅助函数 ----------
  481. def validate_container(self, data):
  482. """验证容器是否存在"""
  483. container = data.get('container_number')
  484. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  485. if not container_obj:
  486. return None, Response({
  487. 'code': '400',
  488. 'message': '托盘编码不存在',
  489. 'data': data
  490. }, status=status.HTTP_400_BAD_REQUEST)
  491. return container_obj, None
  492. def update_container_data(self, container_obj, data):
  493. """更新容器数据"""
  494. serializer = ContainerListPostSerializer(
  495. container_obj,
  496. data=data,
  497. partial=True
  498. )
  499. if serializer.is_valid():
  500. serializer.save()
  501. return True
  502. return False
  503. def is_already_at_target(self, container_obj, current_location):
  504. """检查是否已在目标位置"""
  505. print (current_location)
  506. print (str(container_obj.target_location))
  507. return current_location == str(container_obj.target_location)
  508. def handle_target_reached(self, container_obj, data):
  509. """处理已到达目标位置的逻辑"""
  510. logger.info(f"托盘 {container_obj.container_code} 已在目标位置")
  511. task = self.get_task_by_tasknumber(data)
  512. self.update_pressure_values(task, container_obj)
  513. # if task.working == 1:
  514. # alloca = LocationAllocation()
  515. # alloca.update_batch_goods_in_location_qty(container_obj.container_code, 1)
  516. task = self.process_task_completion(data)
  517. if not task:
  518. return Response({'code': '400', 'message': '任务不存在', 'data': data},
  519. status=status.HTTP_400_BAD_REQUEST)
  520. if task and task.tasktype == 'inbound':
  521. self.update_storage_system(container_obj)
  522. if task and task.tasktype == 'outbound' and task.status == 300:
  523. success = self.handle_outbound_completion(container_obj, task)
  524. if not success:
  525. return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
  526. status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  527. OutboundService.process_next_task()
  528. return Response({
  529. 'code': '200',
  530. 'message': '当前位置已是目标位置',
  531. 'data': data
  532. }, status=status.HTTP_200_OK)
  533. def get_task_by_tasknumber(self, data):
  534. taskNumber = data.get('taskNumber') + 20000000000
  535. task = ContainerWCSModel.objects.filter(tasknumber=taskNumber).first()
  536. if task:
  537. return task
  538. else:
  539. return None
  540. def process_task_completion(self, data):
  541. """处理任务完成状态"""
  542. taskNumber = data.get('taskNumber') + 20000000000
  543. task = ContainerWCSModel.objects.filter(tasknumber=taskNumber).first()
  544. if task:
  545. task.status = 300
  546. task.message = '任务已完成'
  547. task.working = 0
  548. task.save()
  549. return task
  550. def update_pressure_values(self, task, container_obj):
  551. """更新压力值计算"""
  552. if task :
  553. base_location_obj = base_location.objects.get(id=1)
  554. layer = int(container_obj.target_location.split('-')[-1])
  555. pressure_field = f"layer{layer}_pressure"
  556. logger.info(f"更新压力值,压力字段:{pressure_field}")
  557. current_pressure = getattr(base_location_obj, pressure_field, 0)
  558. updated_pressure = max(current_pressure - task.working, 0)
  559. setattr(base_location_obj, pressure_field, updated_pressure)
  560. base_location_obj.save()
  561. def update_storage_system(self, container_obj):
  562. """更新仓储系统状态"""
  563. allocator = LocationAllocation()
  564. location_code = self.get_location_code(container_obj.target_location)
  565. # 链式更新操作
  566. update_operations = [
  567. (allocator.update_location_status, location_code, 'occupied'),
  568. (allocator.update_location_container_link, location_code, container_obj.container_code),
  569. (allocator.update_container_detail_status, container_obj.container_code, 2)
  570. ]
  571. for func, *args in update_operations:
  572. if not func(*args):
  573. logger.error(f"操作失败: {func.__name__}")
  574. return False
  575. return True
  576. def get_location_code(self, target_location):
  577. """从目标位置解析获取位置编码"""
  578. parts = target_location.split('-')
  579. coordinate = f"{int(parts[1])}-{int(parts[2])}-{int(parts[3])}"
  580. return LocationModel.objects.filter(coordinate=coordinate).first().location_code
  581. def handle_new_allocation(self, container_obj, data):
  582. """处理新库位分配逻辑"""
  583. allocator = LocationAllocation()
  584. container_code = container_obj.container_code
  585. # 获取并验证库位分配
  586. location = allocator.get_location_by_status(container_code, data.get('current_location'))
  587. if not location or not self.perform_initial_allocation(allocator, location, container_code):
  588. return Response({'code': '400', 'message': '库位分配失败', 'data': data},
  589. status=status.HTTP_400_BAD_REQUEST)
  590. # 生成目标位置并更新容器
  591. target_location = self.generate_target_location(location)
  592. container_obj.target_location = target_location
  593. container_obj.save()
  594. # 创建任务并返回响应
  595. task = self.create_inbound_task(container_code, data, target_location, location)
  596. return Response({
  597. 'code': '200',
  598. 'message': '任务下发成功',
  599. 'data': task.to_dict()
  600. }, status=status.HTTP_200_OK)
  601. def perform_initial_allocation(self, allocator, location, container_code):
  602. """执行初始库位分配操作"""
  603. operations = [
  604. (allocator.update_location_status, location.location_code, 'reserved'),
  605. (allocator.update_location_group_status, location.location_code),
  606. (allocator.update_batch_status, container_code, '2'),
  607. (allocator.update_location_group_batch, location, container_code),
  608. (allocator.update_location_container_link, location.location_code, container_code),
  609. (allocator.update_container_detail_status, container_code, 2)
  610. ]
  611. for func, *args in operations:
  612. if not func(*args):
  613. logger.error(f"分配操作失败: {func.__name__}")
  614. return False
  615. return True
  616. def generate_target_location(self, location):
  617. """生成目标位置字符串"""
  618. return (
  619. f"{location.warehouse_code}-"
  620. f"{int(location.row):02d}-"
  621. f"{int(location.col):02d}-"
  622. f"{int(location.layer):02d}"
  623. )
  624. def create_inbound_task(self, container_code, data, target_location, location):
  625. """创建入库任务"""
  626. batch_id = LocationAllocation().get_batch(container_code)
  627. self.generate_task(
  628. container_code,
  629. data.get('current_location'),
  630. target_location,
  631. batch_id,
  632. location.c_number
  633. )
  634. task = ContainerWCSModel.objects.get(container=container_code, tasktype='inbound')
  635. self.inport_update_task(task.id, container_code)
  636. return task
  637. @transaction.atomic
  638. def inport_update_task(self, wcs_id,container_id):
  639. try:
  640. task_obj = ContainerWCSModel.objects.filter(id=wcs_id).first()
  641. if task_obj:
  642. container_detail_obj = ContainerDetailModel.objects.filter(container=container_id,is_delete=False).all()
  643. if container_detail_obj:
  644. for detail in container_detail_obj:
  645. # 保存到数据库
  646. batch = BoundDetailModel.objects.filter(bound_batch_id=detail.batch.id).first()
  647. TaskModel.objects.create(
  648. task_wcs = task_obj,
  649. container_detail = detail,
  650. batch_detail = batch
  651. )
  652. logger.info(f"入库任务 {wcs_id} 已更新")
  653. else:
  654. logger.info(f"入库任务 {container_id} 批次不存在")
  655. else:
  656. logger.info(f"入库任务 {wcs_id} 不存在")
  657. except Exception as e:
  658. logger.error(f"处理入库任务时发生错误: {str(e)}", exc_info=True)
  659. return Response(
  660. {'code': '500', 'message': '服务器内部错误', 'data': None},
  661. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  662. )
  663. def handle_outbound_completion(self, container_obj, task):
  664. """处理出库完成后的库位释放和状态更新"""
  665. try:
  666. allocator = LocationAllocation()
  667. location_task = task.current_location
  668. location_row = location_task.split('-')[1]
  669. location_col = location_task.split('-')[2]
  670. location_layer = location_task.split('-')[3]
  671. location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
  672. location_code = location.location_code
  673. # 事务确保原子性
  674. with transaction.atomic():
  675. # 解除库位与托盘的关联
  676. if not allocator.release_location(location_code):
  677. raise Exception("解除库位关联失败")
  678. # 更新库位状态为可用
  679. if not allocator.update_location_status(location_code, 'available'):
  680. raise Exception("库位状态更新失败")
  681. # 更新库位组的统计信息
  682. self.handle_group_location_status(location_code, location.location_group)
  683. # 更新容器状态为已出库(假设状态3表示已出库)
  684. container_obj.status = 3
  685. container_obj.save()
  686. return True
  687. except Exception as e:
  688. logger.error(f"出库完成处理失败: {str(e)}")
  689. return False
  690. def handle_group_location_status(self,location_code,location_group):
  691. """
  692. 处理库位组和库位的关联关系
  693. :param location_code: 库位编码
  694. :param location_group: 库位组编码
  695. :return:
  696. """
  697. # 1. 获取库位空闲状态的库位数目
  698. location_obj_number = LocationModel.objects.filter(
  699. location_group=location_group,
  700. status='available'
  701. ).all().count()
  702. # 2. 获取库位组对象
  703. logger.info(f"库位组 {location_group} 下的库位数目:{location_obj_number}")
  704. # 1. 获取库位和库位组的关联关系
  705. location_group_obj = LocationGroupModel.objects.filter(
  706. group_code=location_group
  707. ).first()
  708. if not location_group_obj:
  709. logger.info(f"库位组 {location_group} 不存在")
  710. return None
  711. else:
  712. if location_obj_number == 0:
  713. # 库位组库位已满,更新库位组状态为full
  714. location_group_obj.status = 'full'
  715. location_group_obj.save()
  716. elif location_obj_number < location_group_obj.max_capacity:
  717. location_group_obj.status = 'occupied'
  718. location_group_obj.save()
  719. else:
  720. location_group_obj.status = 'available'
  721. location_group_obj.current_batch = ''
  722. location_group_obj.current_goods_code = ''
  723. location_group_obj.save()
  724. # PDA组盘入库 将扫描到的托盘编码和批次信息保存到数据库
  725. # 1. 先查询托盘对象,如果不存在,则创建托盘对象
  726. # 2. 循环处理每个批次,查询批次对象,
  727. # 3. 更新批次数据(根据业务规则)
  728. # 4. 保存到数据库
  729. # 5. 保存操作记录到数据库
  730. class ContainerDetailViewSet(viewsets.ModelViewSet):
  731. """
  732. retrieve:
  733. Response a data list(get)
  734. list:
  735. Response a data list(all)
  736. create:
  737. Create a data line(post)
  738. delete:
  739. Delete a data line(delete)
  740. """
  741. # authentication_classes = [] # 禁用所有认证类
  742. # permission_classes = [AllowAny] # 允许任意访问
  743. pagination_class = MyPageNumberPagination
  744. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  745. ordering_fields = ['id', "create_time", "update_time", ]
  746. filter_class = ContainerDetailFilter
  747. def get_project(self):
  748. try:
  749. id = self.kwargs.get('pk')
  750. return id
  751. except:
  752. return None
  753. def get_queryset(self):
  754. id = self.get_project()
  755. if self.request.user:
  756. if id is None:
  757. return ContainerDetailModel.objects.filter( is_delete=False)
  758. else:
  759. return ContainerDetailModel.objects.filter( id=id, is_delete=False)
  760. else:
  761. return ContainerDetailModel.objects.none()
  762. def get_serializer_class(self):
  763. if self.action in ['list', 'destroy','retrieve']:
  764. return ContainerDetailGetSerializer
  765. elif self.action in ['create', 'update']:
  766. return ContainerDetailPostSerializer
  767. else:
  768. return self.http_method_not_allowed(request=self.request)
  769. def create(self, request, *args, **kwargs):
  770. data = self.request.data
  771. from .container_operate import ContainerService
  772. ContainerService.create_container_operation(data,logger=logger)
  773. # 将处理后的数据返回(或根据业务需求保存到数据库)
  774. res_data={
  775. "code": "200",
  776. "msg": "Success Create",
  777. "data": data
  778. }
  779. return Response(res_data, status=200)
  780. def update(self, request, pk):
  781. qs = self.get_object()
  782. data = self.request.data
  783. serializer = self.get_serializer(qs, data=data)
  784. serializer.is_valid(raise_exception=True)
  785. serializer.save()
  786. headers = self.get_success_headers(serializer.data)
  787. return Response(serializer.data, status=200, headers=headers)
  788. def destroy(self, request, pk):
  789. qs = self.get_object()
  790. qs.is_delete = True
  791. qs.save()
  792. return Response({'code': 200,'message': '删除成功', 'data': None}, status=200)
  793. def containerdetail_list(self, request):
  794. """
  795. 获取容器详情列表
  796. """
  797. try:
  798. container_id = request.query_params.get('container')
  799. if not container_id:
  800. return Response(
  801. {'code': 400, 'message': '缺少容器ID参数', 'data': None},
  802. status=status.HTTP_400_BAD_REQUEST
  803. )
  804. # 获取容器对象
  805. try:
  806. container = ContainerListModel.objects.get(id=container_id)
  807. except ContainerListModel.DoesNotExist:
  808. return Response(
  809. {'code': 404, 'message': '指定容器不存在', 'data': None},
  810. status=status.HTTP_404_NOT_FOUND
  811. )
  812. # 查询关联批次明细(排除状态0和3)
  813. details = ContainerDetailModel.objects.filter(
  814. container=container,is_delete=False
  815. ).exclude(
  816. status__in=[0, 3]
  817. ).select_related('batch')
  818. details_serializer = ContainerDetailSimpleGetSerializer(details, many=True)
  819. return Response(
  820. {'code': 200, 'message': 'Success', 'data': details_serializer.data},
  821. status=status.HTTP_200_OK
  822. )
  823. except Exception as e:
  824. return Response(
  825. {'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None},
  826. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  827. )
  828. def locationdetail_list(self, request):
  829. """
  830. 获取库位所处托盘的信息(按批次号 + 数量分组)
  831. 新增批次总量统计功能
  832. """
  833. try:
  834. container_id = request.query_params.get('container')
  835. if not container_id:
  836. return Response(
  837. {'code': 400, 'message': '缺少容器ID参数', 'data': None},
  838. status=status.HTTP_400_BAD_REQUEST
  839. )
  840. # 获取容器对象
  841. try:
  842. container = ContainerListModel.objects.get(id=container_id)
  843. except ContainerListModel.DoesNotExist:
  844. return Response(
  845. {'code': 404, 'message': '指定容器不存在', 'data': None},
  846. status=status.HTTP_404_NOT_FOUND
  847. )
  848. # 查询关联批次明细(排除状态0和3)
  849. details = ContainerDetailModel.objects.filter(
  850. container=container,is_delete=False
  851. ).exclude(
  852. status__in=[0, 3]
  853. ).select_related('batch')
  854. if not details.exists():
  855. return Response(
  856. {'code': 404, 'message': '未找到有效批次数据', 'data': None},
  857. status=status.HTTP_404_NOT_FOUND
  858. )
  859. # 按批次号 + 数量分组统计
  860. batch_dict = {}
  861. batch_qty_dict = defaultdict(int) # 使用默认字典自动初始化
  862. for detail in details:
  863. if not detail.batch:
  864. continue
  865. bound_number = detail.batch.bound_number
  866. goods_qty = detail.goods_qty
  867. # 组合键:批次号 + 当前数量
  868. batch_key = (bound_number, goods_qty)
  869. batch_qty_dict[bound_number] += goods_qty # 自动处理键初始化
  870. # 分组统计
  871. if batch_key not in batch_dict:
  872. batch_obj = BoundBatchModel.objects.filter( bound_number=bound_number).first()
  873. batch_dict[batch_key] = {
  874. "goods_code": detail.goods_code,
  875. "goods_desc": detail.goods_desc,
  876. "goods_qty": goods_qty,
  877. "goods_class": detail.goods_class,
  878. "goods_package": batch_obj.goods_package,
  879. "batch_total_qty": batch_obj.goods_qty,
  880. "batch_total_in_qty": batch_obj.goods_in_qty - batch_obj.goods_out_qty,
  881. "status": detail.status,
  882. "group_qty": 1,
  883. "create_time": detail.create_time,
  884. }
  885. else:
  886. batch_dict[batch_key]["group_qty"] += 1
  887. # 重构数据结构
  888. results = []
  889. for (bound_number, qty), data in batch_dict.items():
  890. results.append({
  891. **data,
  892. "bound_number": bound_number,
  893. "current_qty": qty,
  894. "total_batch_qty": batch_qty_dict[bound_number] # 添加批次总量
  895. })
  896. batch_totals =[]
  897. for bound_number, qty in batch_qty_dict.items():
  898. batch_totals.append({
  899. "bound_number": bound_number,
  900. "total_batch_qty": qty
  901. })
  902. return Response(
  903. {
  904. "code": 200,
  905. "message": "Success",
  906. "data": {
  907. "count": len(results),
  908. "results": results,
  909. "batch_totals": batch_totals # 可选:单独返回批次总量
  910. }
  911. },
  912. status=status.HTTP_200_OK
  913. )
  914. except Exception as e:
  915. return Response(
  916. {'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None},
  917. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  918. )
  919. # 托盘操作历史记录
  920. class ContainerOperateViewSet(viewsets.ModelViewSet):
  921. """
  922. retrieve:
  923. Response a data list(get)
  924. list:
  925. Response a data list(all)
  926. create:
  927. Create a data line(post)
  928. delete:
  929. Delete a data line(delete)
  930. """
  931. # authentication_classes = [] # 禁用所有认证类
  932. # permission_classes = [AllowAny] # 允许任意访问
  933. pagination_class = MyPageNumberPagination
  934. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  935. ordering_fields = ['id', "timestamp" ]
  936. filter_class = ContainerOperationFilter
  937. def get_project(self):
  938. try:
  939. id = self.kwargs.get('pk')
  940. return id
  941. except:
  942. return None
  943. def get_queryset(self):
  944. id = self.get_project()
  945. if self.request.user:
  946. if id is None:
  947. return ContainerOperationModel.objects.filter( is_delete=False)
  948. else:
  949. return ContainerOperationModel.objects.filter( id=id, is_delete=False)
  950. else:
  951. return ContainerOperationModel.objects.none()
  952. def get_serializer_class(self):
  953. if self.action in ['list', 'destroy','retrieve']:
  954. return ContainerOperationGetSerializer
  955. elif self.action in ['create', 'update']:
  956. return ContainerOperationPostSerializer
  957. else:
  958. return self.http_method_not_allowed(request=self.request)
  959. def create(self, request, *args, **kwargs):
  960. data = self.request.data
  961. serializer = self.get_serializer(data=data)
  962. serializer.is_valid(raise_exception=True)
  963. serializer.save()
  964. headers = self.get_success_headers(serializer.data)
  965. return Response(serializer.data, status=200, headers=headers)
  966. def update(self, request, pk):
  967. qs = self.get_object()
  968. data = self.request.data
  969. serializer = self.get_serializer(qs, data=data)
  970. serializer.is_valid(raise_exception=True)
  971. serializer.save()
  972. headers = self.get_success_headers(serializer.data)
  973. return Response(serializer.data, status=200, headers=headers)
  974. # 出库任务生成
  975. class OutboundService:
  976. @staticmethod
  977. def generate_task_id():
  978. """生成唯一任务ID(格式: outbound-年月-顺序号)"""
  979. month = timezone.now().strftime("%Y%m")
  980. last_task = ContainerWCSModel.objects.filter(
  981. tasktype='outbound',
  982. month=int(month)
  983. ).order_by('-sequence').first()
  984. sequence = last_task.sequence + 1 if last_task else 1
  985. return f"outbound-{month}-{sequence:05d}"
  986. @staticmethod
  987. def send_task_to_wcs(task):
  988. """异步发送任务到WCS(非阻塞版本)"""
  989. # 提取任务关键数据用于线程(避免直接传递ORM对象)
  990. task_data = {
  991. 'task_id': task.pk, # 使用主键而不是对象
  992. 'send_data': {
  993. "code":'200',
  994. "message": task.message,
  995. "data":{
  996. "taskid": task.taskid,
  997. "container": task.container,
  998. "current_location": task.current_location,
  999. "target_location": task.target_location,
  1000. "tasktype": task.tasktype,
  1001. "month": task.month,
  1002. "message": task.message,
  1003. "status": task.status,
  1004. "taskNumber": task.tasknumber-20000000000,
  1005. "order_number":task.order_number,
  1006. "sequence":task.sequence
  1007. }
  1008. }
  1009. }
  1010. # 创建并启动线程
  1011. thread = threading.Thread(
  1012. target=OutboundService._async_send_handler,
  1013. kwargs=task_data,
  1014. daemon=True # 守护线程(主程序退出时自动终止)
  1015. )
  1016. thread.start()
  1017. return True # 立即返回表示已开始处理
  1018. @staticmethod
  1019. def _async_send_handler(task_id, send_data):
  1020. """异步处理的实际工作函数"""
  1021. try:
  1022. # 每个线程需要独立的数据库连接
  1023. close_old_connections()
  1024. # 重新获取任务对象(确保使用最新数据)
  1025. task = ContainerWCSModel.objects.get(pk=task_id)
  1026. # 发送第一个请求(不处理结果)
  1027. requests.post(
  1028. "http://127.0.0.1:8008/container/batch/",
  1029. json=send_data,
  1030. timeout=10
  1031. )
  1032. # 发送关键请求
  1033. response = requests.post(
  1034. "http://192.168.18.67:1616/wcs/WebApi/getOutTask",
  1035. json=send_data,
  1036. timeout=10
  1037. )
  1038. # 处理响应
  1039. if response.status_code == 200:
  1040. task.status = 200
  1041. task.save()
  1042. logger.info(f"任务 {task.taskid} 已发送")
  1043. else:
  1044. logger.error(f"WCS返回错误: {response.text}")
  1045. except Exception as e:
  1046. logger.error(f"发送失败: {str(e)}")
  1047. finally:
  1048. close_old_connections() # 清理数据库连接
  1049. @staticmethod
  1050. def create_initial_tasks(container_list,bound_list_id):
  1051. """生成初始任务队列"""
  1052. with transaction.atomic():
  1053. current_WCS = ContainerWCSModel.objects.filter(tasktype='outbound',bound_list_id = bound_list_id).first()
  1054. if current_WCS:
  1055. logger.error(f"当前{bound_list_id}已有出库任务")
  1056. return False
  1057. tasks = []
  1058. start_sequence = ContainerWCSModel.objects.filter(tasktype='outbound').count() + 1
  1059. tasknumber = ContainerWCSModel.objects.filter().count()
  1060. tasknumber_index = 1
  1061. for index, container in enumerate(container_list, start=start_sequence):
  1062. container_obj = ContainerListModel.objects.filter(id =container['container_number']).first()
  1063. if container_obj.current_location != container_obj.target_location:
  1064. logger.error(f"托盘 {container_obj.container_code} 未到达目的地,不生成任务")
  1065. return False
  1066. OutBoundDetail_obj = OutBoundDetailModel.objects.filter(bound_list=bound_list_id,bound_batch_number_id=container['batch_id']).first()
  1067. if not OutBoundDetail_obj:
  1068. logger.error(f"批次 {container['batch_id']} 不存在")
  1069. return False
  1070. month = int(timezone.now().strftime("%Y%m"))
  1071. task = ContainerWCSModel(
  1072. taskid=OutboundService.generate_task_id(),
  1073. batch = OutBoundDetail_obj.bound_batch_number,
  1074. batch_out = OutBoundDetail_obj.bound_batch,
  1075. bound_list = OutBoundDetail_obj.bound_list,
  1076. sequence=index,
  1077. order_number = container['c_number'],
  1078. priority=100,
  1079. tasknumber = month*100000+tasknumber_index+tasknumber,
  1080. container=container_obj.container_code,
  1081. current_location=container_obj.current_location,
  1082. target_location="203",
  1083. tasktype="outbound",
  1084. month=int(timezone.now().strftime("%Y%m")),
  1085. message="等待出库",
  1086. status=100,
  1087. )
  1088. tasknumber_index += 1
  1089. tasks.append(task)
  1090. container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
  1091. container_obj.target_location = task.target_location
  1092. container_obj.save()
  1093. ContainerWCSModel.objects.bulk_create(tasks)
  1094. logger.info(f"已创建 {len(tasks)} 个初始任务")
  1095. @staticmethod
  1096. def insert_new_tasks(new_tasks):
  1097. """动态插入新任务并重新排序"""
  1098. with transaction.atomic():
  1099. pending_tasks = list(ContainerWCSModel.objects.select_for_update().filter(status=100))
  1100. # 插入新任务
  1101. for new_task_data in new_tasks:
  1102. new_task = ContainerWCSModel(
  1103. taskid=OutboundService.generate_task_id(),
  1104. priority=new_task_data.get('priority', 100),
  1105. container=new_task_data['container'],
  1106. current_location=new_task_data['current_location'],
  1107. target_location=new_task_data.get('target_location', 'OUT01'),
  1108. tasktype="outbound",
  1109. month=int(timezone.now().strftime("%Y%m")),
  1110. message="等待出库",
  1111. status=100,
  1112. )
  1113. # 找到插入位置
  1114. insert_pos = 0
  1115. for i, task in enumerate(pending_tasks):
  1116. if new_task.priority < task.priority:
  1117. insert_pos = i
  1118. break
  1119. else:
  1120. insert_pos = len(pending_tasks)
  1121. pending_tasks.insert(insert_pos, new_task)
  1122. # 重新分配顺序号
  1123. for i, task in enumerate(pending_tasks, start=1):
  1124. task.sequence = i
  1125. if task.pk is None:
  1126. task.save()
  1127. else:
  1128. task.save(update_fields=['sequence'])
  1129. logger.info(f"已插入 {len(new_tasks)} 个新任务")
  1130. @staticmethod
  1131. def process_next_task():
  1132. """处理下一个任务"""
  1133. next_task = ContainerWCSModel.objects.filter(status=100).order_by('sequence').first()
  1134. if not next_task:
  1135. logger.info("没有待处理任务")
  1136. return
  1137. allocator = LocationAllocation()
  1138. OutboundService.perform_initial_allocation(allocator, next_task.current_location)
  1139. OutboundService.send_task_to_wcs(next_task)
  1140. def perform_initial_allocation(allocator, location):
  1141. """执行初始库位分配操作"""
  1142. location_row = location.split('-')[1]
  1143. location_col = location.split('-')[2]
  1144. location_layer = location.split('-')[3]
  1145. location_code = LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first().location_code
  1146. if not location_code:
  1147. logger.error(f"未找到库位: {location}")
  1148. operations = [
  1149. (allocator.update_location_status,location_code, 'reserved'),
  1150. (allocator.update_location_group_status,location_code)
  1151. ]
  1152. for func, *args in operations:
  1153. if not func(*args):
  1154. logger.error(f"分配操作失败: {func.__name__}")
  1155. return False
  1156. return True
  1157. # 出库任务下发
  1158. class OutTaskViewSet(APIView):
  1159. """
  1160. # fun:get_out_task:下发出库任务
  1161. # fun:get_batch_count_by_boundlist:获取出库申请下的批次数量
  1162. # fun:generate_location_by_demand:根据出库需求生成出库任务
  1163. """
  1164. # authentication_classes = [] # 禁用所有认证类
  1165. # permission_classes = [AllowAny] # 允许任意访问
  1166. def post(self, request):
  1167. try:
  1168. data = self.request.data
  1169. logger.info(f"收到 WMS 推送数据: {data}")
  1170. # 假设从请求中获取 bound_list_id
  1171. bound_list_id = data.get('bound_list_id')
  1172. # 获取关联的出库批次
  1173. out_batches = OutBatchModel.objects.filter(
  1174. bound_list_id=bound_list_id,
  1175. is_delete=False
  1176. ).select_related('batch_number')
  1177. if not out_batches.exists():
  1178. return Response({"code": "404", "msg": "未找到相关出库批次"}, status=404)
  1179. # 构建批次需求字典
  1180. batch_demand = {
  1181. ob.batch_number_id: {
  1182. 'required': ob.goods_out_qty,
  1183. 'allocated': ob.goods_qty,
  1184. 'remaining': ob.goods_out_qty
  1185. } for ob in out_batches
  1186. }
  1187. # 生成出库任务
  1188. generate_result = self.generate_location_by_demand(
  1189. batch_demand=batch_demand,
  1190. bound_list_id=bound_list_id
  1191. )
  1192. if generate_result['code'] != '200':
  1193. return Response(generate_result, status=500)
  1194. # 创建并处理出库任务
  1195. container_list = generate_result['data']
  1196. # 2. 生成初始任务
  1197. OutboundService.create_initial_tasks(container_list,bound_list_id)
  1198. # 3. 立即发送第一个任务
  1199. OutboundService.process_next_task()
  1200. return Response({"code": "200", "msg": "Success"}, status=200)
  1201. except Exception as e:
  1202. logger.error(f"任务生成失败: {str(e)}")
  1203. return Response({"code": "500", "msg": str(e)}, status=500)
  1204. # 获取出库需求
  1205. def get_batch_count_by_boundlist(self,bound_list_id):
  1206. try:
  1207. bound_list_obj_all = OutBoundDetailModel.objects.filter(bound_list=bound_list_id).all()
  1208. if bound_list_obj_all:
  1209. batch_count_dict = {}
  1210. # 统计批次数量(创建哈希表,去重)
  1211. for batch in bound_list_obj_all:
  1212. if batch.bound_batch_number_id not in batch_count_dict:
  1213. batch_count_dict[batch.bound_batch_number_id] = batch.bound_batch.goods_out_qty
  1214. else:
  1215. batch_count_dict[batch.bound_batch_number_id] += batch.bound_batch.goods_out_qty
  1216. return batch_count_dict
  1217. else:
  1218. logger.error(f"查询批次数量失败: {bound_list_id} 不存在")
  1219. return {}
  1220. except Exception as e:
  1221. logger.error(f"查询批次数量失败: {str(e)}")
  1222. return {}
  1223. def get_location_by_status_and_batch(self,status,bound_id):
  1224. try:
  1225. container_obj = ContainerDetailModel.objects.filter(batch=bound_id,status=status,is_delete=False).all()
  1226. if container_obj:
  1227. container_dict = {}
  1228. # 统计托盘数量(创建哈希表,去重)
  1229. for obj in container_obj:
  1230. if obj.container_id not in container_dict:
  1231. container_dict[obj.container_id] = obj.goods_qty
  1232. else:
  1233. container_dict[obj.container_id] += obj.goods_qty
  1234. return container_dict
  1235. else:
  1236. logger.error(f"查询{status}状态的批次数量失败: {bound_id} 不存在")
  1237. return {}
  1238. except Exception as e:
  1239. logger.error(f"查询{status}状态的批次数量失败: {str(e)}")
  1240. return {}
  1241. def get_order_by_batch(self,container_list,bound_id):
  1242. try:
  1243. container_dict = {}
  1244. for container in container_list:
  1245. location_container = LocationContainerLink.objects.filter(container_id=container,is_active=True).first()
  1246. if location_container:
  1247. location_c_number = location_container.location.c_number
  1248. if container not in container_dict:
  1249. container_dict[container] = {
  1250. "container_number":container,
  1251. "location_c_number":location_c_number,
  1252. "location_id ":location_container.location.id,
  1253. "location_type":location_container.location.location_type,
  1254. "batch_id":bound_id,
  1255. }
  1256. if len(container_dict.keys()) == len(container_list):
  1257. return container_dict
  1258. else:
  1259. logger.error(f"查询批次数量失败: {container_list} 不存在")
  1260. return {}
  1261. except Exception as e:
  1262. logger.error(f"查询批次数量失败: {str(e)}")
  1263. return {}
  1264. except Exception as e:
  1265. logger.error(f"查询{status}状态的批次数量失败: {str(e)}")
  1266. return {}
  1267. def get_container_allocation(self, batch_id):
  1268. """兼容所有数据库的去重方案"""
  1269. # 获取唯一容器ID列表
  1270. container_ids = (
  1271. ContainerDetailModel.objects
  1272. .filter(batch_id=batch_id, status=2,is_delete=False)
  1273. .values_list('container_id', flat=True)
  1274. .distinct()
  1275. )
  1276. # 获取每个容器的最新明细(按id倒序)
  1277. return (
  1278. ContainerDetailModel.objects
  1279. .filter(container_id__in=container_ids, status=2,is_delete=False)
  1280. .select_related('container')
  1281. .prefetch_related(
  1282. Prefetch('container__location_links',
  1283. queryset=LocationContainerLink.objects.select_related('location'),
  1284. to_attr='active_location')
  1285. )
  1286. .order_by('container_id', '-id')
  1287. )
  1288. def generate_location_by_demand(self, batch_demand, bound_list_id):
  1289. try:
  1290. return_data = []
  1291. for batch_id, demand in batch_demand.items():
  1292. # 获取已去重的容器列表
  1293. container_qs = self.get_container_allocation(batch_id)
  1294. # 构建容器信息字典(自动去重)
  1295. container_map = {}
  1296. for cd in container_qs:
  1297. if cd.container_id in container_map:
  1298. container_map[cd.container_id]['goods_qty'] += cd.goods_qty
  1299. continue
  1300. # 获取有效库位信息
  1301. active_location = next(
  1302. (link.location for link in cd.container.active_location
  1303. if link.is_active),
  1304. None
  1305. )
  1306. container_map[cd.container_id] = {
  1307. 'detail': cd,
  1308. 'goods_qty': cd.goods_qty,
  1309. 'container': cd.container,
  1310. 'location': active_location
  1311. }
  1312. # 转换为排序列表
  1313. container_list = list(container_map.values())
  1314. # 多维度排序(优化性能版)
  1315. sorted_containers = sorted(
  1316. container_list,
  1317. key=lambda x: (
  1318. self._get_goods_class_priority(x['detail'].goods_class),
  1319. -(x['location'].c_number if x['location'] else 0),
  1320. # -(x['location'].layer if x['location'] else 0),
  1321. # x['location'].row if x['location'] else 0,
  1322. # x['location'].col if x['location'] else 0
  1323. )
  1324. )
  1325. # 分配逻辑
  1326. remaining = demand['remaining']
  1327. for item in sorted_containers:
  1328. if remaining <= 0:
  1329. break
  1330. # 获取可分配数量
  1331. allocatable = item['goods_qty'] - item['detail'].goods_out_qty
  1332. allocate_qty = min(remaining, allocatable)
  1333. # 记录分配信息
  1334. return_data.append({
  1335. "container_number": item['container'].id,
  1336. "batch_id": batch_id,
  1337. "location_code": item['location'].location_code if item['location'] else 'N/A',
  1338. "allocate_qty": allocate_qty,
  1339. "c_number": item['location'].c_number if item['location'] else 0
  1340. })
  1341. remaining -= allocate_qty
  1342. # 更新数据库状态(需要事务处理)
  1343. self._update_allocation_status(item, allocate_qty)
  1344. return {"code": "200", "msg": "Success", "data": return_data}
  1345. except Exception as e:
  1346. logger.error(f"出库任务生成失败: {str(e)}", exc_info=True)
  1347. return {"code": "500", "msg": str(e)}
  1348. def _get_goods_class_priority(self, goods_class):
  1349. """货物类型优先级权重"""
  1350. return {
  1351. 3: 0, # 散盘最高
  1352. 1: 1, # 成品次之
  1353. 2: 2 # 空盘最低
  1354. }.get(goods_class, 99)
  1355. @transaction.atomic
  1356. def _update_allocation_status(self, item, allocate_qty):
  1357. """事务化更新分配状态"""
  1358. try:
  1359. # 更新容器明细
  1360. ContainerDetailModel.objects.filter(
  1361. id=item['detail'].id ,is_delete=False
  1362. ).update(
  1363. goods_out_qty=F('goods_out_qty') + allocate_qty,
  1364. status=Case(
  1365. When(goods_qty=F('goods_out_qty') + allocate_qty, then=3),
  1366. default=2
  1367. )
  1368. )
  1369. # 更新库位状态
  1370. # if item['location']:
  1371. # LocationModel.objects.filter(
  1372. # id=item['location'].id
  1373. # ).update(
  1374. # current_quantity=F('current_quantity') - allocate_qty
  1375. # # status=Case(
  1376. # # When(current_quantity=F('current_quantity') - allocate_qty, then='available'),
  1377. # # default='occupied'
  1378. # # )
  1379. # )
  1380. return True
  1381. except Exception as e:
  1382. logger.error(f"状态更新失败: {str(e)}")
  1383. return False
  1384. def create_or_update_container_operation(self,container_obj,batch_id,bound_id,to_location,goods_qty,goods_weight):
  1385. try:
  1386. container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,bound_id=bound_id,operation_type="outbound").first()
  1387. if container_operation_obj:
  1388. logger.info(f"[0]查询出库任务: {container_operation_obj.operation_type} ")
  1389. logger.info(f"更新出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
  1390. container_operation_obj.to_location = to_location
  1391. container_operation_obj.goods_qty = goods_qty
  1392. container_operation_obj.goods_weight = goods_weight
  1393. container_operation_obj.save()
  1394. else:
  1395. logger.info(f"创建出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
  1396. batch = BoundBatchModel.objects.filter(id=batch_id).first()
  1397. if not batch:
  1398. return {"code": "500", "msg": f"批次 {batch_id} 不存在"}
  1399. ContainerOperationModel.objects.create(
  1400. month = int(timezone.now().strftime("%Y%m")),
  1401. container = container_obj,
  1402. goods_code = batch.goods_code,
  1403. goods_desc = batch.goods_desc,
  1404. operation_type ="outbound",
  1405. batch_id = batch_id,
  1406. bound_id = bound_id,
  1407. goods_qty = goods_qty,
  1408. goods_weight = goods_weight,
  1409. from_location = container_obj.current_location,
  1410. to_location= to_location,
  1411. timestamp=timezone.now(),
  1412. operator="WMS",
  1413. memo=f"出库需求: {bound_id}, 批次: {batch_id}, 数量: {goods_qty}"
  1414. )
  1415. return {"code": "200", "msg": "Success"}
  1416. except Exception as e:
  1417. return {"code": "500", "msg": str(e)}
  1418. def update_container_detail_out_qty(self,container_obj,batch_id):
  1419. try:
  1420. logger.info(f"[1]更新托盘出库数量: {container_obj.container_code} 批次 {batch_id} ")
  1421. container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,operation_type="outbound").all()
  1422. if not container_operation_obj:
  1423. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务")
  1424. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务"}
  1425. container_detail_obj = ContainerDetailModel.objects.filter(container=container_obj,batch_id=batch_id,status=2,is_delete=False).first()
  1426. if not container_detail_obj:
  1427. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息")
  1428. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息"}
  1429. out_qty = 0
  1430. for obj in container_operation_obj:
  1431. out_qty += obj.goods_qty
  1432. if out_qty >= container_detail_obj.goods_qty:
  1433. out_qty = container_detail_obj.goods_qty
  1434. container_detail_obj.status = 3
  1435. break
  1436. if out_qty == 0:
  1437. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量")
  1438. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量"}
  1439. container_detail_obj.goods_out_qty = out_qty
  1440. container_detail_obj.save()
  1441. return {"code": "200", "msg": "Success"}
  1442. except Exception as e:
  1443. return {"code": "500", "msg": str(e)}
  1444. # 出库任务监测
  1445. class BatchViewSet(viewsets.ModelViewSet):
  1446. authentication_classes = [] # 禁用所有认证类
  1447. permission_classes = [AllowAny] # 允许任意访问
  1448. def wcs_post(self, request, *args, **kwargs):
  1449. data = self.request.data
  1450. logger.info(f"收到 WMS 推送数据: {data}")
  1451. return Response({"code": "200", "msg": "Success"}, status=200)