views.py 88 KB

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