views.py 68 KB

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