views.py 84 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936
  1. from rest_framework import viewsets
  2. from utils.page import MyPageNumberPagination
  3. from utils.datasolve import sumOfList, transportation_calculate
  4. from utils.md5 import Md5
  5. from rest_framework.filters import OrderingFilter
  6. from django_filters.rest_framework import DjangoFilterBackend
  7. from rest_framework.response import Response
  8. from rest_framework.exceptions import APIException
  9. from django.utils import timezone
  10. from django.db import transaction
  11. import logging
  12. from rest_framework import status
  13. from rest_framework.decorators import action
  14. from .models import DeviceModel,LocationModel,LocationGroupModel,LocationContainerLink,LocationChangeLog,alloction_pre,base_location
  15. from bound.models import BoundBatchModel,BoundDetailModel,BoundListModel
  16. from .filter import DeviceFilter,LocationFilter,LocationContainerLinkFilter,LocationChangeLogFilter,LocationGroupFilter
  17. from .serializers import LocationListSerializer,LocationPostSerializer
  18. from .serializers import LocationGroupListSerializer,LocationGroupPostSerializer
  19. # 以后添加模块时,只需要在这里添加即可
  20. from rest_framework.permissions import AllowAny
  21. from container.models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,TaskModel
  22. from django.db.models import Prefetch, Count, Q
  23. import copy
  24. import json
  25. from collections import defaultdict
  26. logger = logging.getLogger(__name__)
  27. from operation_log.views import log_operation,log_failure_operation,log_success_operation
  28. from operation_log.models import OperationLog
  29. # 库位分配
  30. # 入库规则函数
  31. # 逻辑根据批次下的托盘数目来找满足区间范围的库位,按照优先级排序,
  32. class locationViewSet(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 = LocationFilter
  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. prefetch_containers = Prefetch(
  58. 'container_links',
  59. queryset=LocationContainerLink.objects.filter(
  60. is_active=True
  61. ).select_related('container'), # 加载关联的托盘对象
  62. to_attr='active_links' # 新的属性名称
  63. )
  64. if self.request.auth:
  65. if id is None:
  66. log_operation(
  67. request=self.request,
  68. operation_content="查看库位列表",
  69. operation_level="view",
  70. operator=self.request.auth.name if self.request.auth else None,
  71. module_name="库位"
  72. )
  73. return LocationModel.objects.prefetch_related(prefetch_containers).all()
  74. else:
  75. log_operation(
  76. request=self.request,
  77. operation_content=f"查看库位详情 ID:{id}",
  78. operation_level="view",
  79. operator=self.request.auth.name if self.request.auth else None,
  80. object_id=id,
  81. module_name="库位"
  82. )
  83. return LocationModel.objects.prefetch_related(prefetch_containers).filter(id=id)
  84. else:
  85. return LocationModel.objects.none()
  86. def get_serializer_class(self):
  87. if self.action == 'list':
  88. return LocationListSerializer
  89. elif self.action == 'create':
  90. return LocationPostSerializer
  91. elif self.action == 'update':
  92. return LocationPostSerializer
  93. elif self.action =='retrieve':
  94. return LocationListSerializer
  95. def create(self, request, *args, **kwargs):
  96. """创建库位"""
  97. serializer = self.get_serializer(data=request.data)
  98. try:
  99. serializer.is_valid(raise_exception=True)
  100. instance = serializer.save()
  101. log_success_operation(
  102. request=self.request,
  103. operation_content=f"创建库位成功,库位编码: {instance.location_code}",
  104. operation_level="new",
  105. operator=self.request.auth.name if self.request.auth else None,
  106. object_id=instance.id,
  107. module_name="库位"
  108. )
  109. headers = self.get_success_headers(serializer.data)
  110. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  111. except Exception as e:
  112. log_failure_operation(
  113. request=self.request,
  114. operation_content=f"创建库位失败: {str(e)}",
  115. operation_level="new",
  116. operator=self.request.auth.name if self.request.auth else None,
  117. module_name="库位"
  118. )
  119. raise
  120. def destroy(self, request, *args, **kwargs):
  121. """删除库位"""
  122. instance = self.get_object()
  123. location_code = instance.location_code
  124. object_id = instance.id
  125. try:
  126. self.perform_destroy(instance)
  127. log_success_operation(
  128. request=self.request,
  129. operation_content=f"删除库位成功,库位编码: {location_code}",
  130. operation_level="delete",
  131. operator=self.request.auth.name if self.request.auth else None,
  132. object_id=object_id,
  133. module_name="库位"
  134. )
  135. return Response(status=status.HTTP_204_NO_CONTENT)
  136. except Exception as e:
  137. log_failure_operation(
  138. request=self.request,
  139. operation_content=f"删除库位失败,库位编码: {location_code}, 错误: {str(e)}",
  140. operation_level="delete",
  141. operator=self.request.auth.name if self.request.auth else None,
  142. object_id=object_id,
  143. module_name="库位"
  144. )
  145. raise
  146. def update(self, request, *args, **kwargs):
  147. qs = self.get_object()
  148. data = self.request.data
  149. location_code = data.get('location_code')
  150. # 处理库位对象
  151. location_obj = LocationModel.objects.filter(location_code=location_code).first()
  152. if not location_obj:
  153. log_failure_operation(
  154. request=self.request,
  155. operation_content=f"更新库位失败,库位 {location_code} 不存在",
  156. operation_level="update",
  157. operator=self.request.auth.name if self.request.auth else None,
  158. module_name="库位"
  159. )
  160. logger.info(f"库位 {location_code} 不存在")
  161. return Response(
  162. {'code': '400', 'message': '库位不存在', 'data': None},
  163. status=status.HTTP_400_BAD_REQUEST
  164. )
  165. else:
  166. data['id'] = location_obj.id
  167. logger.info(f"库位 {location_code} 已存在")
  168. serializer = self.get_serializer(qs, data=data)
  169. serializer.is_valid(raise_exception=True)
  170. serializer.save()
  171. log_success_operation(
  172. request=self.request,
  173. operation_content=f"更新库位成功,库位 {location_code} 已更新",
  174. operation_level="update",
  175. operator=self.request.auth.name if self.request.auth else None,
  176. object_id=location_obj.id,
  177. module_name="库位"
  178. )
  179. headers = self.get_success_headers(serializer.data)
  180. self.handle_group_location_status(location_code,location_obj.location_group)
  181. return Response(serializer.data, status=200, headers=headers)
  182. def handle_group_location_status(self,location_code,location_group):
  183. """
  184. 处理库位组和库位的关联关系
  185. :param location_code: 库位编码
  186. :param location_group: 库位组编码
  187. :return:
  188. """
  189. # 1. 获取库位空闲状态的库位数目
  190. location_obj_number = LocationModel.objects.filter(
  191. location_group=location_group,
  192. status='available'
  193. ).all().count()
  194. # 2. 获取库位组对象
  195. logger.info(f"库位组 {location_group} 下的库位数目:{location_obj_number}")
  196. # 1. 获取库位和库位组的关联关系
  197. location_group_obj = LocationGroupModel.objects.filter(
  198. group_code=location_group
  199. ).first()
  200. if not location_group_obj:
  201. logger.info(f"库位组 {location_group} 不存在")
  202. return None
  203. else:
  204. if location_obj_number == 0:
  205. # 库位组库位已满,更新库位组状态为full
  206. location_group_obj.status = 'full'
  207. location_group_obj.save()
  208. elif location_obj_number < location_group_obj.max_capacity:
  209. location_group_obj.status = 'occupied'
  210. location_group_obj.save()
  211. else:
  212. location_group_obj.status = 'available'
  213. location_group_obj.save()
  214. def batch_status_location(self, request):
  215. """
  216. 优化版:批量获取库位批次状态
  217. 基于模型结构优化查询
  218. """
  219. layer = request.data.get('layer')
  220. # 使用反向关系名 'container_links' 进行预取
  221. locations = LocationModel.objects.filter(
  222. layer=layer
  223. ).prefetch_related(
  224. Prefetch(
  225. 'container_links', # 使用模型定义的 related_name
  226. queryset=LocationContainerLink.objects.filter(is_active=True)
  227. .select_related('container'),
  228. to_attr='active_links'
  229. )
  230. )
  231. # 收集所有激活链接的托盘ID
  232. container_ids = set()
  233. for loc in locations:
  234. if loc.active_links: # 每个库位最多只有一个激活链接
  235. container_ids.add(loc.active_links[0].container_id)
  236. # 批量查询托盘详情及其批次状态
  237. container_batch_status = defaultdict(dict) # 改为字典存储,避免重复记录
  238. if container_ids:
  239. container_details = ContainerDetailModel.objects.filter(
  240. container_id__in=container_ids ,
  241. is_delete=False
  242. ).select_related('batch').exclude(status=3) # 排除已删除或不合格的托盘
  243. for detail in container_details:
  244. if detail.batch_id:
  245. # 创建唯一标识的键
  246. status_key = (
  247. detail.batch.check_status if detail.batch else "404",
  248. detail.batch.bound_number if detail.batch else "no_batch"
  249. )
  250. # 如果这个状态尚未添加过,或者需要更新
  251. if status_key not in container_batch_status[detail.container_id]:
  252. container_batch_status[detail.container_id][status_key] = (
  253. detail.batch.check_status if detail.batch else "404",
  254. detail.batch.check_time if detail.batch else "no_check_time",
  255. detail.batch.bound_number if detail.batch else "no_batch",
  256. detail.goods_qty-detail.goods_out_qty if detail.goods_qty-detail.goods_out_qty > 0 else 0,
  257. )
  258. else:
  259. # 如果批次状态相同,则更新库位数量
  260. if container_batch_status[detail.container_id][status_key][0] == detail.batch.check_status:
  261. container_batch_status[detail.container_id][status_key] = (
  262. detail.batch.check_status,
  263. max(container_batch_status[detail.container_id][status_key][1], detail.batch.check_time),
  264. detail.batch.bound_number,
  265. container_batch_status[detail.container_id][status_key][3] + (detail.goods_qty-detail.goods_out_qty if detail.goods_qty-detail.goods_out_qty > 0 else 0)
  266. )
  267. # 构造返回数据
  268. return_data = []
  269. for loc in locations:
  270. batch_statuses = []
  271. if loc.active_links:
  272. container_id = loc.active_links[0].container_id
  273. # 从字典中提取值并转换为列表
  274. if container_id in container_batch_status:
  275. batch_statuses = list(container_batch_status[container_id].values())
  276. else:
  277. batch_statuses = [("404", "no_check_time", "no_batch")]
  278. # 使用Django模型自带的model_to_dict转换基础字段
  279. from django.forms.models import model_to_dict
  280. location_data = model_to_dict(loc, fields=[
  281. "id", "shelf_type", "row", "col", "layer", "update_time",
  282. "empty_label", "location_code", "location_group", "location_type",
  283. "status", "max_capacity", "current_quantity", "c_number",
  284. "coordinate", "access_priority", "is_active"
  285. ])
  286. # 添加批次状态字段 - 存储所有信息
  287. location_data["batch_statuses"] = batch_statuses
  288. return_data.append(location_data)
  289. data = {
  290. "code": "200",
  291. "msg": "Success Create",
  292. "data": return_data
  293. }
  294. log_operation(
  295. request=self.request,
  296. operation_content=f"批量获取库位批次状态,层号:{layer}",
  297. operation_level="view",
  298. operator=self.request.auth.name if self.request.auth else None,
  299. module_name="库位"
  300. )
  301. return Response(data, status=200)
  302. class locationGroupViewSet(viewsets.ModelViewSet):
  303. """
  304. retrieve:
  305. Response a data list(get)
  306. list:
  307. Response a data list(all)
  308. create:
  309. Create a data line(post)
  310. delete:
  311. Delete a data line(delete)
  312. """
  313. # authentication_classes = [] # 禁用所有认证类
  314. # permission_classes = [AllowAny] # 允许任意访问
  315. pagination_class = MyPageNumberPagination
  316. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  317. ordering_fields = ['id', "create_time", "update_time", ]
  318. filter_class = LocationGroupFilter
  319. def get_project(self):
  320. try:
  321. id = self.kwargs.get('pk')
  322. return id
  323. except:
  324. return None
  325. def get_queryset(self):
  326. id = self.get_project()
  327. if self.request.auth:
  328. if id is None:
  329. log_operation(
  330. request=self.request,
  331. operation_content="查看库位组列表",
  332. operation_level="view",
  333. operator=self.request.auth.name if self.request.auth else None,
  334. module_name="库位"
  335. )
  336. return LocationGroupModel.objects.filter()
  337. else:
  338. log_operation(
  339. request=self.request,
  340. operation_content=f"查看库位组详情 ID:{id}",
  341. operation_level="view",
  342. operator=self.request.auth.name if self.request.auth else None,
  343. object_id=id,
  344. module_name="库位"
  345. )
  346. return LocationGroupModel.objects.filter(id=id)
  347. else:
  348. return LocationGroupModel.objects.none()
  349. def get_serializer_class(self):
  350. if self.action == 'create':
  351. return LocationGroupPostSerializer
  352. if self.action == 'list':
  353. return LocationGroupListSerializer
  354. elif self.action == 'update':
  355. return LocationGroupPostSerializer
  356. elif self.action =='retrieve':
  357. return LocationGroupListSerializer
  358. def create(self, request, *args, **kwargs):
  359. """创建库位组"""
  360. data = self.request.data.copy()
  361. order_month = str(timezone.now().strftime('%Y%m'))
  362. data['month'] = order_month
  363. serializer = LocationGroupPostSerializer(data=data)
  364. try:
  365. serializer.is_valid(raise_exception=True)
  366. instance = serializer.save()
  367. log_success_operation(
  368. request=self.request,
  369. operation_content=f"创建库位组成功,库位组编码: {instance.group_code}",
  370. operation_level="new",
  371. operator=self.request.auth.name if self.request.auth else None,
  372. object_id=instance.id,
  373. module_name="库位"
  374. )
  375. headers = self.get_success_headers(serializer.data)
  376. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  377. except Exception as e:
  378. log_failure_operation(
  379. request=self.request,
  380. operation_content=f"创建库位组失败: {str(e)}",
  381. operation_level="new",
  382. operator=self.request.auth.name if self.request.auth else None,
  383. module_name="库位"
  384. )
  385. raise
  386. def destroy(self, request, *args, **kwargs):
  387. """删除库位组"""
  388. instance = self.get_object()
  389. group_code = instance.group_code
  390. object_id = instance.id
  391. try:
  392. self.perform_destroy(instance)
  393. log_success_operation(
  394. request=self.request,
  395. operation_content=f"删除库位组成功,库位组编码: {group_code}",
  396. operation_level="delete",
  397. operator=self.request.auth.name if self.request.auth else None,
  398. object_id=object_id,
  399. module_name="库位"
  400. )
  401. return Response(status=status.HTTP_204_NO_CONTENT)
  402. except Exception as e:
  403. log_failure_operation(
  404. request=self.request,
  405. operation_content=f"删除库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
  406. operation_level="delete",
  407. operator=self.request.auth.name if self.request.auth else None,
  408. object_id=object_id,
  409. module_name="库位"
  410. )
  411. raise
  412. def update(self, request, *args, **kwargs):
  413. data = self.request.data
  414. order_month = str(timezone.now().strftime('%Y%m'))
  415. data['month'] = order_month
  416. group_code = data.get('group_code')
  417. # 处理库位组对象
  418. group_obj = LocationGroupModel.objects.filter(group_code=group_code).first()
  419. if group_obj:
  420. data['id'] = group_obj.id
  421. logger.info(f"库位组 {group_code} 已存在")
  422. # 更新现有库位组,支持部分字段更新
  423. serializer = LocationGroupPostSerializer(group_obj, data=data, partial=True)
  424. try:
  425. serializer.is_valid(raise_exception=True)
  426. serializer.save()
  427. log_success_operation(
  428. request=self.request,
  429. operation_content=f"更新库位组成功,库位组编码: {group_code}",
  430. operation_level="update",
  431. operator=self.request.auth.name if self.request.auth else None,
  432. object_id=group_obj.id,
  433. module_name="库位"
  434. )
  435. return Response(serializer.data, status=status.HTTP_200_OK)
  436. except Exception as e:
  437. log_failure_operation(
  438. request=self.request,
  439. operation_content=f"更新库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
  440. operation_level="update",
  441. operator=self.request.auth.name if self.request.auth else None,
  442. object_id=group_obj.id,
  443. module_name="库位"
  444. )
  445. raise
  446. else:
  447. logger.info(f"库位组 {group_code} 不存在,创建库位组对象")
  448. serializer_list = LocationGroupPostSerializer(data=data)
  449. serializer_list.is_valid(raise_exception=True)
  450. serializer_list.save()
  451. log_success_operation(
  452. request=self.request,
  453. operation_content=f"创建库位组成功,库位组编码: {group_code}",
  454. operation_level="new",
  455. operator=self.request.auth.name if self.request.auth else None,
  456. object_id=serializer_list.data.get('id'),
  457. module_name="库位"
  458. )
  459. data['id'] = serializer_list.data.get('id')
  460. return Response(data, status=status.HTTP_201_CREATED)
  461. def check_pallet_consistency(self, request):
  462. """
  463. 检测库位组当前托盘数与激活的Link记录数量是否一致
  464. """
  465. if not request.auth:
  466. return Response(
  467. {'code': '401', 'message': '未授权访问', 'data': None},
  468. status=status.HTTP_401_UNAUTHORIZED
  469. )
  470. only_inconsistent = str(request.query_params.get('only_inconsistent', 'true')).lower() in {'1', 'true', 'yes', 'on'}
  471. warehouse_code = request.query_params.get('warehouse_code')
  472. group_code = request.query_params.get('group_code')
  473. layer_param = request.query_params.get('layer')
  474. filters = {'is_active': True}
  475. if warehouse_code:
  476. filters['warehouse_code'] = warehouse_code
  477. if group_code:
  478. filters['group_code'] = group_code
  479. if layer_param:
  480. try:
  481. filters['layer'] = int(layer_param)
  482. except (TypeError, ValueError):
  483. pass
  484. prefetch_active_links = Prefetch(
  485. 'location_items',
  486. queryset=LocationModel.objects.prefetch_related(
  487. Prefetch(
  488. 'container_links',
  489. queryset=LocationContainerLink.objects.filter(is_active=True),
  490. to_attr='active_links'
  491. )
  492. ),
  493. to_attr='prefetched_locations'
  494. )
  495. groups_qs = LocationGroupModel.objects.filter(**filters).annotate(
  496. active_pallets=Count(
  497. 'location_items__container_links',
  498. filter=Q(location_items__container_links__is_active=True),
  499. distinct=True
  500. )
  501. ).prefetch_related(prefetch_active_links)
  502. total_groups = groups_qs.count()
  503. inconsistent_groups = 0
  504. result_data = []
  505. for group in groups_qs:
  506. locations = getattr(group, 'prefetched_locations', [])
  507. actual_quantity = getattr(group, 'active_pallets', 0)
  508. recorded_quantity = group.current_quantity
  509. is_consistent = recorded_quantity == actual_quantity
  510. if not is_consistent:
  511. inconsistent_groups += 1
  512. if only_inconsistent and is_consistent:
  513. continue
  514. location_details = []
  515. if not is_consistent:
  516. for location in locations:
  517. active_links = getattr(location, 'active_links', [])
  518. active_count = len(active_links)
  519. location_details.append({
  520. 'location_id': location.id,
  521. 'location_code': location.location_code,
  522. 'status': location.status,
  523. 'recorded_quantity': location.current_quantity,
  524. 'link_quantity': active_count,
  525. 'difference': active_count - location.current_quantity
  526. })
  527. result_data.append({
  528. 'group_id': group.id,
  529. 'group_code': group.group_code,
  530. 'group_name': group.group_name,
  531. 'warehouse_code': group.warehouse_code,
  532. 'layer': group.layer,
  533. 'status': group.status,
  534. 'recorded_quantity': recorded_quantity,
  535. 'link_quantity': actual_quantity,
  536. 'difference': actual_quantity - recorded_quantity,
  537. 'is_consistent': is_consistent,
  538. 'locations': location_details
  539. })
  540. log_operation(
  541. request=self.request,
  542. operation_content=f"执行库位组托盘数一致性检测,共检测 {total_groups} 个库位组,发现 {inconsistent_groups} 个异常",
  543. operation_level="view",
  544. operator=self.request.auth.name if self.request.auth else None,
  545. module_name="库位"
  546. )
  547. response_data = {
  548. 'timestamp': timezone.now().isoformat(),
  549. 'summary': {
  550. 'total_groups': total_groups,
  551. 'inconsistent_groups': inconsistent_groups,
  552. 'consistent_groups': total_groups - inconsistent_groups
  553. },
  554. 'data': result_data
  555. }
  556. return Response(response_data, status=status.HTTP_200_OK)
  557. @action(methods=['post'], detail=False, url_path='fix-pallet-consistency')
  558. def fix_pallet_consistency(self, request):
  559. """
  560. 修复库位组托盘数不一致问题:
  561. - 将库位组记录的 current_quantity 校准为实际激活托盘数
  562. - 将组内库位的 current_quantity 校准为各自的激活托盘数
  563. """
  564. if not request.auth:
  565. return Response(
  566. {'code': '401', 'message': '未授权访问', 'data': None},
  567. status=status.HTTP_401_UNAUTHORIZED
  568. )
  569. group_codes = request.data.get('group_codes')
  570. warehouse_code = request.data.get('warehouse_code')
  571. group_code_filter = None
  572. if isinstance(group_codes, (list, tuple, set)):
  573. group_code_filter = [code for code in group_codes if code]
  574. layer_param = request.data.get('layer')
  575. filters = {'is_active': True}
  576. if warehouse_code:
  577. filters['warehouse_code'] = warehouse_code
  578. if layer_param is not None:
  579. try:
  580. filters['layer'] = int(layer_param)
  581. except (TypeError, ValueError):
  582. pass
  583. if group_code_filter:
  584. filters['group_code__in'] = group_code_filter
  585. prefetch_active_links = Prefetch(
  586. 'location_items',
  587. queryset=LocationModel.objects.prefetch_related(
  588. Prefetch(
  589. 'container_links',
  590. queryset=LocationContainerLink.objects.filter(is_active=True),
  591. to_attr='active_links'
  592. )
  593. ),
  594. to_attr='prefetched_locations'
  595. )
  596. annotated_groups = list(
  597. LocationGroupModel.objects.filter(**filters)
  598. .annotate(
  599. active_pallets=Count(
  600. 'location_items__container_links',
  601. filter=Q(location_items__container_links__is_active=True),
  602. distinct=True,
  603. )
  604. )
  605. .values('id', 'group_code', 'active_pallets')
  606. )
  607. if not annotated_groups:
  608. return Response(
  609. {
  610. 'timestamp': timezone.now().isoformat(),
  611. 'summary': {
  612. 'total_groups': 0,
  613. 'processed_groups': 0,
  614. 'fixed_groups': 0,
  615. 'fixed_locations': 0,
  616. 'skipped_groups': 0,
  617. },
  618. 'details': {
  619. 'fixed_groups': [],
  620. 'fixed_locations': [],
  621. 'skipped_groups': [],
  622. }
  623. },
  624. status=status.HTTP_200_OK
  625. )
  626. group_stats_map = {
  627. item['id']: {
  628. 'group_code': item['group_code'],
  629. 'active_pallets': item.get('active_pallets') or 0,
  630. }
  631. for item in annotated_groups
  632. }
  633. group_ids = list(group_stats_map.keys())
  634. groups_qs = LocationGroupModel.objects.filter(id__in=group_ids).prefetch_related(prefetch_active_links)
  635. total_groups = len(group_ids)
  636. fixed_groups = []
  637. fixed_locations = []
  638. skipped_groups = []
  639. processed_groups = 0
  640. try:
  641. with transaction.atomic():
  642. for group in groups_qs.select_for_update():
  643. stats = group_stats_map.get(group.id, {})
  644. actual_quantity = stats.get('active_pallets', 0)
  645. group_code = stats.get('group_code') or group.group_code
  646. processed_groups += 1
  647. recorded_quantity = group.current_quantity or 0
  648. if recorded_quantity != actual_quantity:
  649. old_quantity = recorded_quantity
  650. group.current_quantity = actual_quantity
  651. group.save(update_fields=['current_quantity'])
  652. fixed_groups.append({
  653. 'group_id': group.id,
  654. 'group_code': group_code,
  655. 'old_quantity': old_quantity,
  656. 'new_quantity': actual_quantity,
  657. 'difference': actual_quantity - old_quantity,
  658. })
  659. locations = getattr(group, 'prefetched_locations', [])
  660. for location in locations:
  661. active_links = getattr(location, 'active_links', [])
  662. active_count = len(active_links)
  663. recorded_loc_qty = location.current_quantity or 0
  664. if recorded_loc_qty != active_count:
  665. old_loc_qty = recorded_loc_qty
  666. LocationModel.objects.filter(pk=location.id).update(
  667. current_quantity=active_count,
  668. update_time=timezone.now()
  669. )
  670. fixed_locations.append({
  671. 'location_id': location.id,
  672. 'location_code': location.location_code,
  673. 'group_code': group_code,
  674. 'old_quantity': old_loc_qty,
  675. 'new_quantity': active_count,
  676. 'difference': active_count - old_loc_qty,
  677. })
  678. else:
  679. skipped_groups.append({
  680. 'group_id': group.id,
  681. 'group_code': group_code,
  682. 'recorded_quantity': recorded_quantity,
  683. 'actual_quantity': actual_quantity,
  684. })
  685. summary = {
  686. 'total_groups': total_groups,
  687. 'processed_groups': processed_groups,
  688. 'fixed_groups': len(fixed_groups),
  689. 'fixed_locations': len(fixed_locations),
  690. 'skipped_groups': len(skipped_groups),
  691. }
  692. log_operation(
  693. request=self.request,
  694. operation_content=(
  695. f"执行托盘数修复:共处理 {processed_groups} 个库位组,"
  696. f"修复 {len(fixed_groups)} 个库位组,调整 {len(fixed_locations)} 个库位"
  697. ),
  698. operation_level="update",
  699. operator=self.request.auth.name if self.request.auth else None,
  700. module_name="库位"
  701. )
  702. return Response(
  703. {
  704. 'timestamp': timezone.now().isoformat(),
  705. 'summary': summary,
  706. 'details': {
  707. 'fixed_groups': fixed_groups,
  708. 'fixed_locations': fixed_locations,
  709. 'skipped_groups': skipped_groups,
  710. }
  711. },
  712. status=status.HTTP_200_OK
  713. )
  714. except Exception as exc:
  715. log_failure_operation(
  716. request=self.request,
  717. operation_content=f"托盘数修复失败: {str(exc)}",
  718. operation_level="update",
  719. operator=self.request.auth.name if self.request.auth else None,
  720. module_name="库位"
  721. )
  722. raise
  723. class LocationAllocation:
  724. # 入库规则函数
  725. # fun:get_pallet_count_by_batch: 根据托盘码查询批次下托盘总数
  726. # fun:get_left_locationGroup_number_by_type: 获取每层库位组剩余数量
  727. # fun:get_location_type: 根据托盘数目获取库位类型
  728. # fun:update_location_container_link: 更新库位和托盘的关联关系
  729. # fun:update_location_group_batch: 更新库位组的批次
  730. # fun:update_batch_status: 更新批次状态yes/no
  731. # fun:update_location_status: 更新库位状态和
  732. # fun:up
  733. # fun:get_batch_status: 获取批次状态
  734. # fun:get_batch: 获取批次
  735. # fun:get_location_list_remainder: 获取可用库位的c_number列表
  736. # fun
  737. # fun:get_location_by_type_remainder: 根据库位类型获取库位
  738. # fun:get_location_by_type: 第一次入库,根据库位类型获取库位
  739. # fun:get_location_by_status: 根据库位状态获取库位
  740. def get_pallet_count_by_batch(self, container_code):
  741. """
  742. 根据托盘码查询批次下托盘总数
  743. :param container_code: 要查询的托盘码
  744. :return: 所属批次下的托盘总数
  745. """
  746. # 1. 通过托盘码获取托盘详情
  747. container = ContainerListModel.objects.filter(
  748. container_code=container_code
  749. ).first()
  750. if not container:
  751. logger.error(f"托盘 {container_code} 不存在")
  752. return None
  753. # 2. 获取关联的批次明细
  754. container_detail = ContainerDetailModel.objects.filter(
  755. container=container.id,is_delete=False
  756. ).exclude(status = 3).first()
  757. if not container_detail:
  758. logger.error(f"托盘 {container_code} 未组盘")
  759. return None
  760. batch_container = ContainerDetailModel.objects.filter(
  761. batch = container_detail.batch.id,
  762. is_delete = False,
  763. status = 1
  764. ).all()
  765. # 统计批次下的不同托盘 item.contianer_id
  766. batch_container_count = 0
  767. container_ids = []
  768. for item in batch_container:
  769. if item.container_id not in container_ids:
  770. batch_container_count = batch_container_count + 1
  771. container_ids.append(item.container_id)
  772. batch_item = BoundBatchModel.objects.filter( bound_number = container_detail.batch.bound_number).first()
  773. if not batch_item:
  774. print(f"批次号获取失败!")
  775. return None
  776. batch_item.container_number = batch_container_count
  777. batch_item.save()
  778. return batch_container_count
  779. def get_left_locationGroup_number_by_type(self):
  780. """
  781. 获取每层库位组剩余数量
  782. :return:
  783. """
  784. try:
  785. # 定义库位组和层号
  786. group = ['T1', 'T2', 'S4', 'T4', 'T5']
  787. layer = [1, 2, 3]
  788. # 初始化结果列表,包含三个空字典对应三个层
  789. left_number = [{} for _ in layer]
  790. for item in group:
  791. for idx, layer_num in enumerate(layer):
  792. # 检查库位组是否存在(不考虑状态)
  793. exists = LocationGroupModel.objects.filter(
  794. group_type=item,
  795. layer=layer_num
  796. ).exists()
  797. if not exists:
  798. print(f"库位组 {item}_{layer_num} 不存在")
  799. left_number[idx][item] = 0
  800. else:
  801. # 统计可用状态的库位组数量
  802. count = LocationGroupModel.objects.filter(
  803. group_type=item,
  804. layer=layer_num,
  805. status='available'
  806. ).count()
  807. left_number[idx][item] = count
  808. return left_number
  809. except Exception as e:
  810. logger.error(f"获取库位组剩余数量失败:{str(e)}")
  811. print(f"获取库位组剩余数量失败:{str(e)}")
  812. return None
  813. def update_location_container_link(self,location_code,container_code, request=None):
  814. """
  815. 更新库位和托盘的关联关系
  816. :param location_code: 库位编码
  817. :param container_code: 托盘编码
  818. :param request: 请求对象(可选)
  819. :return:
  820. """
  821. try:
  822. # 1. 获取库位和托盘的关联关系
  823. location = LocationModel.objects.filter(
  824. location_code=location_code
  825. ).first()
  826. container = ContainerListModel.objects.filter(
  827. container_code=container_code
  828. ).first()
  829. # 2. 如果库位和托盘的关联关系不存在,创建新的关联关系
  830. if not LocationContainerLink.objects.filter(location=location).exists():
  831. location_container_link = LocationContainerLink(
  832. location=location,
  833. container=container
  834. )
  835. location_container_link.save()
  836. print(f"更新库位和托盘的关联关系成功!")
  837. # 记录操作日志
  838. try:
  839. if request:
  840. log_success_operation(
  841. request=request,
  842. operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
  843. operation_level="other",
  844. module_name="库位分配"
  845. )
  846. else:
  847. OperationLog.objects.create(
  848. operator="系统自动",
  849. operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
  850. operation_level="other",
  851. operation_result="success",
  852. module_name="库位分配"
  853. )
  854. except Exception as log_error:
  855. logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
  856. return True
  857. # 3. 更新库位和托盘的关联关系
  858. else:
  859. LocationContainerLink.objects.filter(location=location).update(location=location, container=container)
  860. print(f"更新库位和托盘的关联关系成功!")
  861. # 记录操作日志
  862. try:
  863. if request:
  864. log_success_operation(
  865. request=request,
  866. operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
  867. operation_level="update",
  868. module_name="库位分配"
  869. )
  870. else:
  871. OperationLog.objects.create(
  872. operator="系统自动",
  873. operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
  874. operation_level="update",
  875. operation_result="success",
  876. module_name="库位分配"
  877. )
  878. except Exception as log_error:
  879. logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
  880. return True
  881. except Exception as e:
  882. logger.error(f"更新库位和托盘的关联关系失败:{str(e)}")
  883. print(f"更新库位和托盘的关联关系失败:{str(e)}")
  884. # 记录失败日志
  885. try:
  886. if request:
  887. log_failure_operation(
  888. request=request,
  889. operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
  890. operation_level="update",
  891. module_name="库位分配"
  892. )
  893. else:
  894. OperationLog.objects.create(
  895. operator="系统自动",
  896. operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
  897. operation_level="update",
  898. operation_result="failure",
  899. module_name="库位分配"
  900. )
  901. except Exception as log_error:
  902. logger.error(f"记录失败日志失败: {str(log_error)}")
  903. return False
  904. def update_container_detail_status(self,container_code,status):
  905. try:
  906. # 1. 获取托盘
  907. container = ContainerListModel.objects.filter(
  908. container_code=container_code
  909. ).first()
  910. if not container:
  911. print(f"托盘 {container_code} 不存在")
  912. return False
  913. # 2. 更新托盘状态
  914. container_detail = ContainerDetailModel.objects.filter(
  915. container=container.id,is_delete=False
  916. ).exclude(status=3).first()
  917. if not container_detail:
  918. print(f"托盘 {container_code} 未组盘_from update_container_detail_status")
  919. return False
  920. container_detail.status = status
  921. container_detail.save()
  922. print(f"更新托盘状态成功!")
  923. return True
  924. except Exception as e:
  925. logger.error(f"更新托盘状态失败:{str(e)}")
  926. print(f"更新托盘状态失败:{str(e)}")
  927. return False
  928. def update_location_group_batch(self,location,container_code):
  929. """
  930. :param location: 库位对象
  931. :param container_code: 托盘码
  932. :return:
  933. """
  934. try:
  935. # 1. 获取库位组
  936. location_group = LocationGroupModel.objects.filter(
  937. group_code=location.location_group
  938. ).first()
  939. if not location_group:
  940. print(f"库位组获取失败!")
  941. return False
  942. # 2. 更新库位组的批次
  943. bound_number=self.get_batch(container_code)
  944. if not bound_number:
  945. print(f"批次号获取失败!")
  946. return False
  947. location_group.current_batch = bound_number
  948. location_group.save()
  949. print(f"更新库位组的批次成功!")
  950. return True
  951. except Exception as e:
  952. logger.error(f"更新库位组的批次失败:{str(e)}")
  953. print(f"更新库位组的批次失败:{str(e)}")
  954. return False
  955. def update_location_status(self,location_code,status, request=None):
  956. """
  957. 更新库位状态
  958. :param location_code: 库位编码
  959. :param status: 库位状态
  960. :param request: 请求对象(可选)
  961. :return:
  962. """
  963. try:
  964. # 1. 获取库位
  965. location = LocationModel.objects.filter(
  966. location_code=location_code
  967. ).first()
  968. if not location:
  969. print(f"库位获取失败!")
  970. return False
  971. # 2. 更新库位状态
  972. old_status = location.status
  973. location.status = status
  974. location.save()
  975. print(f"更新库位状态成功!")
  976. # 记录操作日志(状态变更敏感操作)
  977. try:
  978. if request:
  979. log_success_operation(
  980. request=request,
  981. operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
  982. operation_level="update",
  983. object_id=str(location.id),
  984. module_name="库位分配"
  985. )
  986. else:
  987. OperationLog.objects.create(
  988. operator="系统自动",
  989. operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
  990. operation_level="update",
  991. operation_result="success",
  992. object_id=str(location.id),
  993. module_name="库位分配"
  994. )
  995. except Exception as log_error:
  996. logger.error(f"记录库位状态更新日志失败: {str(log_error)}")
  997. return True
  998. except Exception as e:
  999. logger.error(f"更新库位状态失败:{str(e)}")
  1000. print(f"更新库位状态失败:{str(e)}")
  1001. # 记录失败日志
  1002. try:
  1003. if request:
  1004. log_failure_operation(
  1005. request=request,
  1006. operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
  1007. operation_level="update",
  1008. module_name="库位分配"
  1009. )
  1010. else:
  1011. OperationLog.objects.create(
  1012. operator="系统自动",
  1013. operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
  1014. operation_level="update",
  1015. operation_result="failure",
  1016. module_name="库位分配"
  1017. )
  1018. except Exception as log_error:
  1019. logger.error(f"记录失败日志失败: {str(log_error)}")
  1020. return False
  1021. def update_group_status_reserved(self,location_group_list, request=None):
  1022. """
  1023. 更新库位组状态为预留
  1024. :param location_group_list: 库位组对象列表
  1025. :param request: 请求对象(可选)
  1026. :return:
  1027. """
  1028. try:
  1029. reserved_groups = []
  1030. for location_group in location_group_list:
  1031. # 1. 获取库位组
  1032. if not location_group:
  1033. print(f"库位组获取失败!")
  1034. return False
  1035. # 2. 更新库位组状态
  1036. location_group_id = location_group.split('_')[1]
  1037. location_group_item = LocationGroupModel.objects.filter(
  1038. id=location_group_id
  1039. ).first()
  1040. if not location_group_item:
  1041. print(f"库位组 {location_group} 不存在")
  1042. return False
  1043. # 3. 更新库位组状态
  1044. old_status = location_group_item.status
  1045. location_group_item.status = 'reserved'
  1046. location_group_item.save()
  1047. reserved_groups.append(f"{location_group_item.group_code}({old_status}->reserved)")
  1048. # 记录预留库位组操作日志(敏感操作)
  1049. try:
  1050. if request:
  1051. log_success_operation(
  1052. request=request,
  1053. operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
  1054. operation_level="update",
  1055. module_name="库位分配"
  1056. )
  1057. else:
  1058. OperationLog.objects.create(
  1059. operator="系统自动",
  1060. operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
  1061. operation_level="update",
  1062. operation_result="success",
  1063. module_name="库位分配"
  1064. )
  1065. except Exception as log_error:
  1066. logger.error(f"记录预留库位组日志失败: {str(log_error)}")
  1067. return True
  1068. except Exception as e:
  1069. logger.error(f"更新库位组状态失败:{str(e)}")
  1070. print(f"更新库位组状态失败:{str(e)}")
  1071. # 记录失败日志
  1072. try:
  1073. if request:
  1074. log_failure_operation(
  1075. request=request,
  1076. operation_content=f"预留库位组失败,错误: {str(e)}",
  1077. operation_level="update",
  1078. module_name="库位分配"
  1079. )
  1080. else:
  1081. OperationLog.objects.create(
  1082. operator="系统自动",
  1083. operation_content=f"预留库位组失败,错误: {str(e)}",
  1084. operation_level="update",
  1085. operation_result="failure",
  1086. module_name="库位分配"
  1087. )
  1088. except Exception as log_error:
  1089. logger.error(f"记录失败日志失败: {str(log_error)}")
  1090. return False
  1091. def update_location_group_status(self, location_code):
  1092. """
  1093. 更新库位组状态
  1094. :param location_code: 库位编码
  1095. :return:
  1096. """
  1097. try:
  1098. # 1. 获取库位
  1099. location = LocationModel.objects.filter(
  1100. location_code=location_code
  1101. ).first()
  1102. if not location:
  1103. print(f"库位获取失败!")
  1104. return False
  1105. # 2. 获取库位组
  1106. location_group = LocationGroupModel.objects.filter(
  1107. group_code=location.location_group
  1108. ).first()
  1109. if not location_group:
  1110. print(f"库位组获取失败!")
  1111. return False
  1112. current=0
  1113. for location_item in location_group.location_items.all():
  1114. if location_item.status != 'available':
  1115. current=current + 1
  1116. # 3. 更新库位组状态
  1117. if current == 0:
  1118. location_group.status = 'available'
  1119. elif current == location_group.max_capacity:
  1120. location_group.status = 'full'
  1121. else:
  1122. location_group.status = 'occupied'
  1123. location_group.current_goods_quantity = sum(
  1124. [loc.current_quantity for loc in location_group.location_items.all()]
  1125. )
  1126. location_group.current_quantity = current
  1127. location_group.save()
  1128. print(f"更新库位组状态成功!")
  1129. return True
  1130. except Exception as e:
  1131. logger.error(f"更新库位组状态失败:{str(e)}")
  1132. print(f"更新库位组状态失败:{str(e)}")
  1133. def update_batch_status(self,container_code,status, request=None):
  1134. """
  1135. 更新批次状态
  1136. :param container_code: 托盘编码
  1137. :param status: 批次状态
  1138. :param request: 请求对象(可选)
  1139. :return:
  1140. """
  1141. try:
  1142. # 1. 通过托盘码获取托盘详情
  1143. container = ContainerListModel.objects.filter(
  1144. container_code=container_code
  1145. ).first()
  1146. if not container:
  1147. logger.error(f"托盘 {container_code} 不存在")
  1148. print(f"托盘 {container_code} 不存在")
  1149. return None
  1150. # 2. 获取关联的批次明细
  1151. container_detail = ContainerDetailModel.objects.filter(
  1152. container=container.id,is_delete=False
  1153. ).exclude(status=3).first()
  1154. if not container_detail:
  1155. print (f"托盘 {container_code} 未组盘")
  1156. logger.error(f"托盘 {container_code} 未组盘_from update_batch_status")
  1157. return None
  1158. # 3. 更新批次状态
  1159. batch = container_detail.batch
  1160. old_status = batch.status
  1161. batch.status = status
  1162. batch.save()
  1163. print(f"更新批次状态成功!")
  1164. # 记录操作日志(批次状态变更敏感操作)
  1165. try:
  1166. if request:
  1167. log_success_operation(
  1168. request=request,
  1169. operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
  1170. operation_level="update",
  1171. object_id=str(batch.id),
  1172. module_name="库位分配"
  1173. )
  1174. else:
  1175. OperationLog.objects.create(
  1176. operator="系统自动",
  1177. operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
  1178. operation_level="update",
  1179. operation_result="success",
  1180. object_id=str(batch.id),
  1181. module_name="库位分配"
  1182. )
  1183. except Exception as log_error:
  1184. logger.error(f"记录批次状态更新日志失败: {str(log_error)}")
  1185. return True
  1186. except Exception as e:
  1187. logger.error(f"更新批次状态失败:{str(e)}")
  1188. print(f"更新批次状态失败:{str(e)}")
  1189. # 记录失败日志
  1190. try:
  1191. if request:
  1192. log_failure_operation(
  1193. request=request,
  1194. operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
  1195. operation_level="update",
  1196. module_name="库位分配"
  1197. )
  1198. else:
  1199. OperationLog.objects.create(
  1200. operator="系统自动",
  1201. operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
  1202. operation_level="update",
  1203. operation_result="failure",
  1204. module_name="库位分配"
  1205. )
  1206. except Exception as log_error:
  1207. logger.error(f"记录失败日志失败: {str(log_error)}")
  1208. return False
  1209. # def update_batch_goods_in_location_qty(self,container_code,taskworking):
  1210. # """
  1211. # 更新批次库位入库数量
  1212. # :param container_code: 托盘码
  1213. # :param goods_in_location_qty: 库位入库数量
  1214. # :return:
  1215. # """
  1216. # try:
  1217. # # 1. 通过托盘码获取托盘详情
  1218. # container = ContainerListModel.objects.filter(
  1219. # container_code=container_code
  1220. # ).first()
  1221. # if not container:
  1222. # logger.error(f"托盘 {container_code} 不存在")
  1223. # print(f"托盘 {container_code} 不存在")
  1224. # return None
  1225. # # 2. 获取关联的批次明细
  1226. # container_detail = ContainerDetailModel.objects.filter(
  1227. # container=container.id,is_delete=False
  1228. # ).exclude(status=3).all()
  1229. # if not container_detail:
  1230. # print (f"托盘 {container_code} 未组盘")
  1231. # logger.error(f"托盘 {container_code} 未组盘_from update_batch_goods_in_location_qty")
  1232. # return None
  1233. # for item in container_detail:
  1234. # if item.goods_class == 2:
  1235. # continue
  1236. # item.batch.goods_in_location_qty += item.goods_qty * taskworking
  1237. # item.batch.save()
  1238. # print(f"更新批次库位入库数量成功!")
  1239. # return True
  1240. # except Exception as e:
  1241. # logger.error(f"更新批次库位入库数量失败:{str(e)}")
  1242. # print(f"更新批次库位入库数量失败:{str(e)}")
  1243. # return False
  1244. def get_batch_status(self,container_code):
  1245. """
  1246. 获取批次状态
  1247. :param container_code: 托盘码
  1248. :return: 批次状态
  1249. """
  1250. # 1. 通过托盘码获取托盘详情
  1251. container = ContainerListModel.objects.filter(
  1252. container_code=container_code
  1253. ).first()
  1254. if not container:
  1255. logger.error(f"托盘 {container_code} 不存在")
  1256. print(f"托盘 {container_code} 不存在")
  1257. return None
  1258. # 2. 获取关联的批次明细
  1259. container_detail = ContainerDetailModel.objects.filter(
  1260. container=container.id,is_delete=False
  1261. ).exclude(status=3).first()
  1262. if not container_detail:
  1263. print (f"托盘 {container_code} 未组盘")
  1264. logger.error(f"托盘 {container_code} 未组盘_from get_batch_status")
  1265. return None
  1266. batch_status = container_detail.batch.status
  1267. return batch_status
  1268. def get_batch(self,container_code):
  1269. """
  1270. 获取批次
  1271. :param container_code: 托盘码
  1272. :return: 批次
  1273. """
  1274. # 1. 通过托盘码获取托盘详情
  1275. container = ContainerListModel.objects.filter(
  1276. container_code=container_code
  1277. ).first()
  1278. if not container:
  1279. logger.error(f"托盘 {container_code} 不存在")
  1280. print(f"托盘 {container_code} 不存在")
  1281. return None
  1282. # 2. 获取关联的批次明细
  1283. container_detail = ContainerDetailModel.objects.filter(
  1284. container=container.id,is_delete=False
  1285. ).exclude(status=3).first()
  1286. if not container_detail:
  1287. print (f"托盘 {container_code} 未组盘")
  1288. logger.error(f"托盘 {container_code} 未组盘_from get_batch")
  1289. return None
  1290. batch = container_detail.batch.bound_number
  1291. return batch
  1292. def get_location_list_remainder(self, location_group_list,container_code):
  1293. """
  1294. 获取可用库位的c_number列表
  1295. :param location_list: 库位对象列表
  1296. :return: 可用库位编号列表
  1297. """
  1298. if not location_group_list:
  1299. return None
  1300. min_c_number=1000
  1301. min_c_number_index=1000
  1302. current_task = self.get_current_finish_task(container_code)
  1303. print(f"[1]当前已完成任务: {current_task}")
  1304. # 按压力排序
  1305. sorted_pressure = sorted(
  1306. [(0, current_task[0]), (1, current_task[1]), (2, current_task[2])],
  1307. key=lambda x: (-x[1], x[0])
  1308. )
  1309. # 交换第一和第二个元素的位置
  1310. sorted_pressure[0], sorted_pressure[1] = sorted_pressure[1], sorted_pressure[0]
  1311. print(f"[2]任务排序: {sorted_pressure}")
  1312. print(f"[3]当前选择:{sorted_pressure[0][0]+1}")
  1313. location_type_dict = json.loads(self.divide_solution_by_layer(location_group_list))
  1314. # print(f"库位类型分配方案: {location_type_dict}")
  1315. for layer, _ in sorted_pressure:
  1316. if not location_type_dict.get(str(layer+1)):
  1317. continue
  1318. # print(f"当前层: {layer+1}")
  1319. # print(f"当前层库位组: {location_type_dict[str(layer+1)].keys()}")
  1320. for group_id in location_type_dict[str(layer+1)].keys():
  1321. location_group = LocationGroupModel.objects.filter(
  1322. id=group_id,
  1323. ).first()
  1324. if not location_group:
  1325. continue
  1326. location_list = location_group.location_items.filter(
  1327. status='available'
  1328. ).all().order_by('c_number')
  1329. if not location_list:
  1330. print(f"当前层库位组 {location_group.group_code} 可用库位: None")
  1331. continue
  1332. # 提取所有库位的 c_number
  1333. c_numbers = [loc.c_number for loc in location_list]
  1334. print(f"当前层库位组 {location_group.group_code} 可用库位: {c_numbers}")
  1335. # 更新任务完成数目
  1336. current_task[layer] = current_task[layer] + 1
  1337. self.update_current_finish_task(container_code,current_task)
  1338. return location_list[0]
  1339. def get_location_type(self, container_code, request=None):
  1340. """
  1341. 智能库位分配核心算法
  1342. :param container_code: 托盘码
  1343. :param request: 请求对象(可选)
  1344. :return: 库位类型分配方案
  1345. """
  1346. try:
  1347. batch = self.get_batch(container_code)
  1348. if not batch:
  1349. logger.error("批次信息获取失败")
  1350. # 记录失败日志
  1351. try:
  1352. if request:
  1353. log_failure_operation(
  1354. request=request,
  1355. operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
  1356. operation_level="other",
  1357. module_name="库位分配算法"
  1358. )
  1359. else:
  1360. OperationLog.objects.create(
  1361. operator="系统自动",
  1362. operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
  1363. operation_level="other",
  1364. operation_result="failure",
  1365. module_name="库位分配算法"
  1366. )
  1367. except Exception as log_error:
  1368. logger.error(f"记录算法失败日志失败: {str(log_error)}")
  1369. return None
  1370. # 检查已有分配方案
  1371. existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
  1372. if existing_solution:
  1373. # 记录使用已有分配方案
  1374. try:
  1375. if request:
  1376. log_operation(
  1377. request=request,
  1378. operation_content=f"使用已有库位分配方案,托盘编码: {container_code}, 批次号: {batch}",
  1379. operation_level="view",
  1380. module_name="库位分配算法"
  1381. )
  1382. except Exception as log_error:
  1383. logger.error(f"记录使用已有方案日志失败: {str(log_error)}")
  1384. return existing_solution.layer_pre_type
  1385. # 获取关键参数
  1386. total_pallets = self.get_pallet_count_by_batch(container_code)
  1387. layer_capacity = self.get_left_locationGroup_number_by_type()
  1388. current_pressure = self.get_current_pressure()
  1389. # 测试参数
  1390. # total_pallets = 30
  1391. # layer_capacity = [{'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}, {'T1': 0, 'T2': 0, 'S4': 0, 'T4': 0, 'T5': 21}, {'T1': 29, 'T2': 14, 'S4': 10, 'T4': 27, 'T5': 27}]
  1392. # current_pressure = [1,0,0]
  1393. print(f"[1]托盘数目: {total_pallets}")
  1394. print(f"[2]层容量: {layer_capacity}")
  1395. # print(f"[3]当前压力: {current_pressure}")
  1396. # 定义库位容量表
  1397. LOCATION_CAPACITY = {'T1':1, 'T2':2, 'T4':4, 'S4':4, 'T5':5}
  1398. def allocate(remain, path, pressure,real_pressure,layer_capacity_state, depth=0):
  1399. # 终止条件
  1400. if remain <= 0:
  1401. return [path,real_pressure]
  1402. # 深拷贝当前层容量状态
  1403. new_layer_capacity = copy.deepcopy(layer_capacity_state)
  1404. # print(f"[2]当前剩余: {new_layer_capacity}")
  1405. # 压力平衡系数
  1406. balance_factor = 1.0 - (0.1 * min(depth, 5))
  1407. # 层选择策略
  1408. print (f"[3]当前压力: {pressure}")
  1409. layer_priority = sorted(
  1410. [(0, pressure[0]), (1, pressure[1]), (2, pressure[2])],
  1411. key=lambda x: (x[1] * balance_factor, x[0])
  1412. )
  1413. for layer, _ in layer_priority:
  1414. # 生成候选库位类型(按效率和容量排序)
  1415. # 排序键函数 :
  1416. # min(x[1], remain) 计算当前库位类型的容量 c 和剩余数量 remain 中的较小值。
  1417. # -min(x[1], remain) 和 -x[1] 都使用了负号,这意味着排序是按降序进行的。
  1418. # 首先按 -min(x[1], remain) 排序,即优先选择容量与剩余数量更接近的库位类型。
  1419. # 如果有多个库位类型的容量与剩余数量相同,则按 -x[1] 排序,即优先选择容量更大的库位类型。
  1420. print(f"[4]当前层: {layer+1}, 剩余: {remain}, 容量状态: {new_layer_capacity[layer]}")
  1421. candidates = sorted(
  1422. [(t, c) for t, c in LOCATION_CAPACITY.items()
  1423. if new_layer_capacity[layer].get(t,0) > 0],
  1424. key=lambda x: (abs(x[1]-remain), -x[1])
  1425. )
  1426. print(f"[4]候选库位类型: {candidates}")
  1427. for loc_type, cap in candidates:
  1428. # 更新容量状态
  1429. updated_capacity = copy.deepcopy(new_layer_capacity)
  1430. updated_capacity[layer][loc_type] -= 1 # 占用一个库位组
  1431. # 允许适度空间浪费(当剩余<2时)
  1432. # effective_cap = min(cap, remain) if (cap - remain) < 2 else cap
  1433. effective_cap = min(cap, remain)
  1434. if effective_cap <= remain:
  1435. new_remain = remain - effective_cap
  1436. new_pressure = pressure.copy()
  1437. for i in range(0, 3):
  1438. new_pressure[i] -=1 if new_pressure[i] > 0 else 0
  1439. new_pressure[layer] += effective_cap # 按实际存放数计算压力,此时别的楼层压力可能降下来了
  1440. real_pressure[layer] += effective_cap # 实际压力
  1441. result = allocate(
  1442. new_remain,
  1443. path + [f"{layer+1}_{loc_type}"],
  1444. new_pressure,
  1445. real_pressure,
  1446. updated_capacity,
  1447. depth + 1
  1448. )
  1449. if result:
  1450. print (f"[5]分配方案: {result}")
  1451. return result
  1452. return None
  1453. # 执行分配
  1454. allocation = allocate(total_pallets, [], [current_pressure[0], current_pressure[1],current_pressure[2]],[current_pressure[0], current_pressure[1],current_pressure[2]], layer_capacity)
  1455. if not allocation:
  1456. logger.error("无法生成有效分配方案")
  1457. # 记录失败日志
  1458. try:
  1459. if request:
  1460. log_failure_operation(
  1461. request=request,
  1462. operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
  1463. operation_level="other",
  1464. module_name="库位分配算法"
  1465. )
  1466. else:
  1467. OperationLog.objects.create(
  1468. operator="系统自动",
  1469. operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
  1470. operation_level="other",
  1471. operation_result="failure",
  1472. module_name="库位分配算法"
  1473. )
  1474. except Exception as log_error:
  1475. logger.error(f"记录算法失败日志失败: {str(log_error)}")
  1476. return None
  1477. # 保存分配方案
  1478. allocation_json = self.divide_solution_by_layer(allocation[0])
  1479. print(f"[6]分配方案: {allocation_json}")
  1480. solution = alloction_pre(
  1481. batch_number=batch,
  1482. layer_pre_type =allocation_json
  1483. )
  1484. solution_pressure, created = base_location.objects.get_or_create(
  1485. id=1,
  1486. defaults={
  1487. 'layer1_pressure': 0,
  1488. 'layer2_pressure': 0,
  1489. 'layer3_pressure': 0
  1490. }
  1491. )
  1492. solution_pressure.layer1_pressure = allocation[1][0]
  1493. solution_pressure.layer2_pressure = allocation[1][1]
  1494. solution_pressure.layer3_pressure = allocation[1][2]
  1495. solution.save()
  1496. solution_pressure.save()
  1497. # 记录算法执行成功日志(关键操作)
  1498. try:
  1499. allocation_summary = f"批次号: {batch}, 托盘数: {total_pallets}, 分配方案: {len(allocation[0])}个库位组, 压力分布: L1={allocation[1][0]}, L2={allocation[1][1]}, L3={allocation[1][2]}"
  1500. if request:
  1501. log_success_operation(
  1502. request=request,
  1503. operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
  1504. operation_level="other",
  1505. module_name="库位分配算法"
  1506. )
  1507. else:
  1508. OperationLog.objects.create(
  1509. operator="系统自动",
  1510. operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
  1511. operation_level="other",
  1512. operation_result="success",
  1513. module_name="库位分配算法"
  1514. )
  1515. except Exception as log_error:
  1516. logger.error(f"记录算法成功日志失败: {str(log_error)}")
  1517. return [loc.split('_')[1] for loc in allocation[0]]
  1518. except Exception as e:
  1519. logger.error(f"分配算法异常:{str(e)}")
  1520. # 记录异常日志
  1521. try:
  1522. if request:
  1523. log_failure_operation(
  1524. request=request,
  1525. operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
  1526. operation_level="other",
  1527. module_name="库位分配算法"
  1528. )
  1529. else:
  1530. OperationLog.objects.create(
  1531. operator="系统自动",
  1532. operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
  1533. operation_level="other",
  1534. operation_result="failure",
  1535. module_name="库位分配算法"
  1536. )
  1537. except Exception as log_error:
  1538. logger.error(f"记录算法异常日志失败: {str(log_error)}")
  1539. return None
  1540. def divide_solution_by_layer(self, data):
  1541. # 统计所有存在的层级
  1542. layer_counts = defaultdict(lambda: defaultdict(int))
  1543. existing_layers = set()
  1544. for item in data:
  1545. # 分割层级和类型
  1546. try:
  1547. layer, loc_type = item.split('_')
  1548. layer_num = int(layer)
  1549. existing_layers.add(layer_num)
  1550. layer_counts[layer_num][loc_type] += 1
  1551. except (ValueError, IndexError):
  1552. continue # 跳过无效格式的数据
  1553. # 确定最大层级(至少包含1层)
  1554. max_layer = max(existing_layers) if existing_layers else 1
  1555. # 构建包含所有层级的最终结果
  1556. final_result = {}
  1557. for layer in range(1, max_layer + 1):
  1558. final_result[str(layer)] = dict(layer_counts.get(layer, {}))
  1559. return json.dumps(final_result, indent=2)
  1560. def get_current_pressure(self):
  1561. """获取实时工作压力"""
  1562. last_solution = base_location.objects.order_by('-id').first()
  1563. if not last_solution:
  1564. base_location.objects.create(
  1565. layer1_pressure=0,
  1566. layer2_pressure=0,
  1567. layer3_pressure=0,
  1568. ).save()
  1569. return [
  1570. last_solution.layer1_pressure if last_solution else 0,
  1571. last_solution.layer2_pressure if last_solution else 0,
  1572. last_solution.layer3_pressure if last_solution else 0,
  1573. ]
  1574. def get_current_finish_task(self,container):
  1575. batch = self.get_batch(container)
  1576. if not batch:
  1577. return None
  1578. solution = alloction_pre.objects.filter(batch_number=batch).first()
  1579. if not solution:
  1580. return None
  1581. return [solution.layer1_task_finish_number,solution.layer2_task_finish_number,solution.layer3_task_finish_number]
  1582. def update_current_finish_task(self,container,task_finish_number):
  1583. batch = self.get_batch(container)
  1584. if not batch:
  1585. return None
  1586. solution = alloction_pre.objects.filter(batch_number=batch).first()
  1587. if not solution:
  1588. return None
  1589. solution.layer1_task_finish_number = task_finish_number[0]
  1590. solution.layer2_task_finish_number = task_finish_number[1]
  1591. solution.layer3_task_finish_number = task_finish_number[2]
  1592. solution.save()
  1593. return True
  1594. def get_location_by_type(self, location_type_list, start_location, container_code):
  1595. """
  1596. 根据库位类型获取库位,先根据工作压力找出最空闲的层,看下这层有没有工作,如果有,就从里面找,如果没有,则跳过该层,继续找下一层
  1597. :param location_type_list: 库位分配方案
  1598. {
  1599. "1": {
  1600. "S4": 1
  1601. },
  1602. "2": {},
  1603. "3": {
  1604. "T5": 1
  1605. }
  1606. }
  1607. :param start_location: 起始位置(决定优先级排序方式)
  1608. :return: 符合条件的库位列表
  1609. """
  1610. locations = []
  1611. # 检查已有分配方案
  1612. existing_solution = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first()
  1613. if existing_solution.layer_solution_type:
  1614. print(f"[0]已有库位分配方案:{existing_solution.layer_solution_type}")
  1615. return existing_solution.layer_solution_type
  1616. for layer, location_type_dict in location_type_list.items():
  1617. if not location_type_dict:
  1618. continue
  1619. # 获取库位类型列表
  1620. location_type = list(location_type_dict.keys())
  1621. demand_number = sum(location_type_dict.values())
  1622. print (f"[1]层{layer} 需求数量: {demand_number}, 库位: {location_type}")
  1623. location_groups = LocationGroupModel.objects.filter(
  1624. group_type__in=location_type,
  1625. layer=layer,
  1626. status='available'
  1627. )
  1628. if not location_groups:
  1629. print(f"层{layer} 无库位")
  1630. # 根据起始位置选择排序字段
  1631. if start_location == '203':
  1632. ordered_groups = location_groups.order_by('left_priority')
  1633. elif start_location == '103':
  1634. ordered_groups = location_groups.order_by('right_priority')
  1635. else:
  1636. ordered_groups = location_groups.none()
  1637. number = 0
  1638. for location_group in ordered_groups:
  1639. if number >= demand_number:
  1640. break
  1641. locations.append(f"{layer}_{location_group.id}")
  1642. number += 1
  1643. existing_solution.layer_solution_type = locations
  1644. existing_solution.save()
  1645. print(f"[2]分配方案: {locations}")
  1646. return locations if locations else None
  1647. def get_location_by_status(self,container_code,start_location, request=None):
  1648. """
  1649. 根据库位状态获取库位
  1650. :param container_code: 托盘编码
  1651. :param start_location: 起始库位 if in1 优先考虑left_priority, if in2 优先考虑right_priority 就是获取库位组列表之后进行排序
  1652. :param request: 请求对象(可选)
  1653. :return: 库位列表
  1654. """
  1655. # 1. 获取批次状态 1 为已组盘 2 为部分入库 3 为全部入库
  1656. status = self.get_batch_status(container_code)
  1657. #
  1658. if status == 1:
  1659. # 2. 获取库位组
  1660. print(f"[1]第一次入库")
  1661. # 重新获取最新数据
  1662. self.get_location_type(container_code, request)
  1663. location_type_list = json.loads(alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_pre_type)
  1664. location_list = self.get_location_by_type(location_type_list,start_location,container_code)
  1665. # 预定这些库组
  1666. self.update_group_status_reserved(location_list, request)
  1667. location_min_value = self.get_location_list_remainder(location_list,container_code)
  1668. print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
  1669. # 记录库位分配成功日志
  1670. try:
  1671. if request:
  1672. log_success_operation(
  1673. request=request,
  1674. operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
  1675. operation_level="other",
  1676. object_id=str(location_min_value.id),
  1677. module_name="库位分配算法"
  1678. )
  1679. else:
  1680. OperationLog.objects.create(
  1681. operator="系统自动",
  1682. operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
  1683. operation_level="other",
  1684. operation_result="success",
  1685. object_id=str(location_min_value.id),
  1686. module_name="库位分配算法"
  1687. )
  1688. except Exception as log_error:
  1689. logger.error(f"记录库位分配日志失败: {str(log_error)}")
  1690. return location_min_value
  1691. elif status == 2:
  1692. # 3. 获取部分入库库位
  1693. print (f"部分入库")
  1694. location_list = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_solution_type
  1695. location_min_value = self.get_location_list_remainder(location_list,container_code)
  1696. print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
  1697. # 记录库位分配成功日志
  1698. try:
  1699. if request:
  1700. log_success_operation(
  1701. request=request,
  1702. operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
  1703. operation_level="other",
  1704. object_id=str(location_min_value.id),
  1705. module_name="库位分配算法"
  1706. )
  1707. else:
  1708. OperationLog.objects.create(
  1709. operator="系统自动",
  1710. operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
  1711. operation_level="other",
  1712. operation_result="success",
  1713. object_id=str(location_min_value.id),
  1714. module_name="库位分配算法"
  1715. )
  1716. except Exception as log_error:
  1717. logger.error(f"记录库位分配日志失败: {str(log_error)}")
  1718. return location_min_value
  1719. def release_location(self, location_code, request=None):
  1720. """释放库位并更新关联数据"""
  1721. try:
  1722. location = LocationModel.objects.get(location_code=location_code)
  1723. links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
  1724. if not links:
  1725. logger.error(f"库位{location_code}未关联托盘")
  1726. return True
  1727. container_id = links.container_id
  1728. print(f"释放库位: {location_code}, 关联托盘: {container_id}")
  1729. # 解除关联并标记为非活跃
  1730. links.is_active = False
  1731. links.save()
  1732. # 更新库位状态为可用
  1733. location.status = 'available'
  1734. location.save()
  1735. # 记录释放库位操作日志(敏感操作)
  1736. try:
  1737. if request:
  1738. log_success_operation(
  1739. request=request,
  1740. operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
  1741. operation_level="update",
  1742. object_id=str(location.id),
  1743. module_name="库位分配"
  1744. )
  1745. else:
  1746. OperationLog.objects.create(
  1747. operator="系统自动",
  1748. operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
  1749. operation_level="update",
  1750. operation_result="success",
  1751. object_id=str(location.id),
  1752. module_name="库位分配"
  1753. )
  1754. except Exception as log_error:
  1755. logger.error(f"记录释放库位日志失败: {str(log_error)}")
  1756. return True
  1757. except Exception as e:
  1758. logger.error(f"释放库位失败: {str(e)}")
  1759. # 记录失败日志
  1760. try:
  1761. if request:
  1762. log_failure_operation(
  1763. request=request,
  1764. operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
  1765. operation_level="update",
  1766. module_name="库位分配"
  1767. )
  1768. else:
  1769. OperationLog.objects.create(
  1770. operator="系统自动",
  1771. operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
  1772. operation_level="update",
  1773. operation_result="failure",
  1774. module_name="库位分配"
  1775. )
  1776. except Exception as log_error:
  1777. logger.error(f"记录失败日志失败: {str(log_error)}")
  1778. return False