views.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. from rest_framework.views import APIView
  2. from rest_framework.response import Response
  3. from rest_framework import status
  4. from django.utils import timezone
  5. from datetime import timedelta
  6. from .services import LocationStatisticsService
  7. from .models import LocationGroupStatistics
  8. from django.db import transaction
  9. from django.db.models import Count, Q, Prefetch
  10. from decimal import Decimal, InvalidOperation
  11. import logging
  12. logger = logging.getLogger(__name__)
  13. from .serializers import LocationStatisticsSerializer, LocationGroupStatisticsSerializer
  14. class LocationStatisticsView(APIView):
  15. """货位统计API视图"""
  16. def get(self, request):
  17. """获取货位统计信息"""
  18. warehouse_code = request.GET.get('warehouse_code')
  19. layer = request.GET.get('layer')
  20. # 尝试从参数转换层数
  21. if layer is not None:
  22. try:
  23. layer = int(layer)
  24. except ValueError:
  25. layer = None
  26. # 从数据库获取最新统计
  27. statistics = LocationStatisticsService.get_latest_statistics(warehouse_code, layer)
  28. serializer = LocationStatisticsSerializer(statistics, many=True)
  29. response_data = {
  30. 'timestamp': timezone.now().isoformat(),
  31. 'data': serializer.data
  32. }
  33. return Response(response_data)
  34. def post(self, request):
  35. """手动触发统计计算"""
  36. warehouse_code = request.data.get('warehouse_code')
  37. layer = request.data.get('layer')
  38. # 计算统计信息
  39. stats_data = LocationStatisticsService.calculate_location_statistics(warehouse_code, layer)
  40. # 保存到数据库
  41. count = LocationStatisticsService.save_statistics_to_db(stats_data)
  42. return Response({
  43. 'message': f'成功统计了{count}条记录',
  44. 'timestamp': timezone.now().isoformat()
  45. }, status=status.HTTP_201_CREATED)
  46. class LocationStatisticsHistoryView(APIView):
  47. """货位统计历史数据API视图"""
  48. def get(self, request):
  49. """获取历史统计信息"""
  50. warehouse_code = request.GET.get('warehouse_code')
  51. layer = request.GET.get('layer')
  52. hours = int(request.GET.get('hours', 24)) # 默认最近24小时
  53. end_time = timezone.now()
  54. start_time = end_time - timedelta(hours=hours)
  55. statistics = LocationStatisticsService.get_statistics_by_time_range(
  56. start_time, end_time, warehouse_code, layer
  57. )
  58. serializer = LocationStatisticsSerializer(statistics, many=True)
  59. return Response({
  60. 'period': f'{start_time} - {end_time}',
  61. 'data': serializer.data
  62. })
  63. class LocationGroupStatisticsView(APIView):
  64. """货位组统计API视图"""
  65. def get(self, request):
  66. """获取货位组统计信息"""
  67. warehouse_code = request.GET.get('warehouse_code')
  68. layer = request.GET.get('layer')
  69. # 新增过滤参数
  70. min_utilization = float(request.GET.get('min_utilization', 0)) # 最小使用率,默认0
  71. min_used_locations = int(request.GET.get('min_used_locations', 0)) # 最小已用货位数,默认0
  72. filters = {}
  73. if warehouse_code:
  74. filters['warehouse_code'] = warehouse_code
  75. if layer:
  76. try:
  77. filters['layer'] = int(layer)
  78. except ValueError:
  79. pass
  80. # 获取最新的组统计
  81. from django.db.models import Max
  82. latest_stats = LocationGroupStatistics.objects.filter(
  83. **filters
  84. ).values('warehouse_code', 'layer', 'location_group').annotate(
  85. latest_time=Max('statistic_time')
  86. )
  87. group_stats = []
  88. for stat in latest_stats:
  89. latest_record = LocationGroupStatistics.objects.filter(
  90. warehouse_code=stat['warehouse_code'],
  91. layer=stat['layer'],
  92. location_group=stat['location_group'],
  93. statistic_time=stat['latest_time']
  94. ).first()
  95. # 根据参数过滤
  96. if latest_record:
  97. # 检查使用率条件
  98. utilization_ok = latest_record.utilization_rate >= min_utilization
  99. # 检查已用货位数条件
  100. used_locations_ok = latest_record.used_locations >= min_used_locations
  101. if utilization_ok and used_locations_ok:
  102. group_stats.append(latest_record)
  103. serializer = LocationGroupStatisticsSerializer(group_stats, many=True)
  104. return Response({
  105. 'timestamp': timezone.now().isoformat(),
  106. 'filters': {
  107. 'min_utilization': min_utilization,
  108. 'min_used_locations': min_used_locations,
  109. 'total_records': len(group_stats)
  110. },
  111. 'data': serializer.data
  112. })
  113. class LocationConsistencyCheckView(APIView):
  114. """库位一致性检查API"""
  115. def post(self, request):
  116. warehouse_code = request.data.get('warehouse_code')
  117. layer_param = request.GET.get('layer')
  118. layer = None
  119. if layer_param is not None:
  120. try:
  121. layer_value = int(layer_param)
  122. if layer_value > 0:
  123. layer = layer_value
  124. except (TypeError, ValueError):
  125. layer = None
  126. auto_fix = self._to_bool(request.data.get('auto_fix', False))
  127. fix_scope = request.data.get('fix_scope')
  128. checker = LocationConsistencyChecker(
  129. warehouse_code,
  130. layer,
  131. auto_fix,
  132. fix_scope=fix_scope
  133. )
  134. checker.check_all()
  135. return Response({
  136. 'success': True,
  137. 'data': checker.generate_report(),
  138. 'auto_fix': auto_fix,
  139. 'fix_scope': checker.fix_scope
  140. })
  141. @staticmethod
  142. def _to_bool(value):
  143. if isinstance(value, bool):
  144. return value
  145. if isinstance(value, str):
  146. return value.strip().lower() in {'1', 'true', 'yes', 'y', 'on'}
  147. if isinstance(value, int):
  148. return value != 0
  149. return False
  150. class LocationConsistencyChecker:
  151. TARGET_LOCATION_TYPES = ['T5', 'T4', 'S4', 'T2', 'T1']
  152. """
  153. 库位一致性检测器
  154. 用于检测库位状态与Link记录的一致性以及库位组的状态一致性
  155. """
  156. def __init__(self, warehouse_code=None, layer=None, auto_fix=False, fix_scope=None):
  157. """
  158. 初始化检测器
  159. Args:
  160. warehouse_code: 指定仓库代码,如果为None则检测所有仓库
  161. layer: 指定楼层,如果为None则检测所有楼层
  162. auto_fix: 是否自动修复检测到的问题
  163. fix_scope: 修复范围(locations/details/groups),None表示全部
  164. """
  165. self.warehouse_code = warehouse_code
  166. self.layer = layer
  167. if self.layer is not None:
  168. try:
  169. layer_int = int(self.layer)
  170. self.layer = layer_int if layer_int > 0 else None
  171. except (TypeError, ValueError):
  172. self.layer = None
  173. self.auto_fix = auto_fix
  174. if isinstance(fix_scope, (list, tuple, set)):
  175. fix_scope = [scope for scope in fix_scope if scope in {'locations', 'groups', 'details'}]
  176. elif isinstance(fix_scope, str):
  177. if fix_scope == 'all':
  178. fix_scope = None
  179. elif fix_scope in {'locations', 'groups', 'details'}:
  180. fix_scope = [fix_scope]
  181. else:
  182. fix_scope = None
  183. else:
  184. fix_scope = None
  185. self.fix_scope = fix_scope or ['locations', 'groups', 'details']
  186. self.results = {
  187. 'check_time': timezone.now(),
  188. 'location_errors': [],
  189. 'group_errors': [],
  190. 'detail_errors': [],
  191. 'fixed_locations': [],
  192. 'fixed_groups': [],
  193. 'fixed_details': [],
  194. 'repair_errors': [],
  195. 'summary': {
  196. 'total_locations': 0,
  197. 'checked_locations': 0,
  198. 'error_locations': 0,
  199. 'total_groups': 0,
  200. 'checked_groups': 0,
  201. 'error_groups': 0,
  202. 'total_details': 0,
  203. 'checked_details': 0,
  204. 'error_details': 0,
  205. 'fixed_location_count': 0,
  206. 'fixed_group_count': 0,
  207. 'fixed_detail_count': 0
  208. }
  209. }
  210. def check_all(self):
  211. """执行所有检测"""
  212. logger.info(f"开始库位一致性检测,仓库: {self.warehouse_code}, 楼层: {self.layer}")
  213. # 检测库位状态与Link记录的一致性
  214. self.check_location_link_consistency()
  215. # 检测托盘明细状态一致性
  216. self.check_container_detail_status()
  217. # 检测库位组状态一致性
  218. self.check_group_consistency()
  219. # 如果启用自动修复,执行修复
  220. if self.auto_fix:
  221. self.fix_detected_issues()
  222. logger.info(f"库位一致性检测完成,发现{self.results['summary']['error_locations']}个库位问题,"
  223. f"{self.results['summary']['error_groups']}个库位组问题,"
  224. f"{self.results['summary']['error_details']}条托盘明细问题")
  225. return self.results
  226. def check_location_link_consistency(self):
  227. """检测库位状态与Link记录的一致性"""
  228. from bin.models import LocationModel, LocationContainerLink
  229. # 构建查询条件
  230. filters = {'is_active': True, 'location_type__in': self.TARGET_LOCATION_TYPES}
  231. if self.warehouse_code:
  232. filters['warehouse_code'] = self.warehouse_code
  233. if self.layer is not None:
  234. filters['layer'] = self.layer
  235. # 获取库位并预取Link记录
  236. locations = LocationModel.objects.filter(**filters).prefetch_related('container_links')
  237. self.results['summary']['total_locations'] = locations.count()
  238. for location in locations:
  239. self.results['summary']['checked_locations'] += 1
  240. # 获取该库位的活跃Link记录数量
  241. active_links_count = location.container_links.filter(is_active=True).count()
  242. # 根据状态判断是否一致
  243. is_consistent, expected_status, error_type = self._check_location_consistency(
  244. location.status, active_links_count
  245. )
  246. if not is_consistent:
  247. self.results['summary']['error_locations'] += 1
  248. self.results['location_errors'].append({
  249. 'location_id': location.id,
  250. 'location_code': location.location_code,
  251. 'warehouse_code': location.warehouse_code,
  252. 'current_status': location.status,
  253. 'expected_status': expected_status,
  254. 'active_links_count': active_links_count,
  255. 'layer': location.layer,
  256. 'row': location.row,
  257. 'col': location.col,
  258. 'location_group': location.location_group,
  259. 'error_type': error_type,
  260. 'detected_at': timezone.now()
  261. })
  262. def _check_location_consistency(self, current_status, active_links_count):
  263. """
  264. 检查单个库位的状态一致性
  265. Returns:
  266. tuple: (是否一致, 预期状态, 错误类型)
  267. """
  268. if current_status == 'available':
  269. # available状态应该没有活跃的Link记录
  270. if active_links_count > 0:
  271. return False, 'occupied', 'available_but_has_links'
  272. elif current_status in ['occupied','reserved']:
  273. # occupied状态应该至少有一个活跃的Link记录
  274. if active_links_count != 1:
  275. return False, 'available', 'occupied_but_no_links'
  276. elif current_status in ['disabled', 'maintenance']:
  277. # 这些特殊状态可以有Link记录,但通常不应该有
  278. if active_links_count > 0:
  279. return False, current_status, 'special_status_has_links'
  280. return True, current_status, None
  281. def check_container_detail_status(self):
  282. """检测托盘明细状态的一致性"""
  283. from container.models import ContainerDetailModel
  284. from bin.models import LocationContainerLink
  285. details_qs = (
  286. ContainerDetailModel.objects.filter(is_delete=False)
  287. .select_related('container', 'batch')
  288. .prefetch_related(
  289. Prefetch(
  290. 'container__location_links',
  291. queryset=LocationContainerLink.objects.filter(is_active=True).select_related('location'),
  292. to_attr='active_links'
  293. )
  294. )
  295. )
  296. total_checked = 0
  297. for detail in details_qs:
  298. if not detail.container:
  299. continue
  300. if not self._detail_in_scope(detail):
  301. continue
  302. goods_qty = self._to_decimal(detail.goods_qty)
  303. goods_out_qty = self._to_decimal(detail.goods_out_qty)
  304. remaining_qty = goods_qty - goods_out_qty
  305. expected_status, error_type = self._determine_detail_status(remaining_qty)
  306. total_checked += 1
  307. self.results['summary']['checked_details'] += 1
  308. if expected_status is None:
  309. continue
  310. if detail.status != expected_status:
  311. self.results['summary']['error_details'] += 1
  312. self.results['detail_errors'].append({
  313. 'detail_id': detail.id,
  314. 'container_id': detail.container_id,
  315. 'container_code': getattr(detail.container, 'container_code', None),
  316. 'batch_id': detail.batch_id,
  317. 'batch_number': getattr(detail.batch, 'bound_number', None) if detail.batch else None,
  318. 'goods_qty': str(goods_qty),
  319. 'goods_out_qty': str(goods_out_qty),
  320. 'remaining_qty': str(remaining_qty),
  321. 'current_status': detail.status,
  322. 'current_status_display': self._status_display(detail.status),
  323. 'expected_status': expected_status,
  324. 'expected_status_display': self._status_display(expected_status),
  325. 'error_type': error_type,
  326. 'detected_at': timezone.now()
  327. })
  328. self.results['summary']['total_details'] = total_checked
  329. def _detail_in_scope(self, detail):
  330. """判断托盘明细是否在当前检测范围内"""
  331. if not (self.warehouse_code or self.layer):
  332. return True
  333. active_links = getattr(detail.container, 'active_links', None)
  334. if not active_links:
  335. return False
  336. warehouse_ok = True
  337. layer_ok = True
  338. if self.warehouse_code:
  339. warehouse_ok = any(link.location.warehouse_code == self.warehouse_code for link in active_links)
  340. if self.layer is not None:
  341. layer_ok = any(link.location.layer == self.layer for link in active_links)
  342. return warehouse_ok and layer_ok
  343. def _to_decimal(self, value):
  344. if isinstance(value, Decimal):
  345. return value
  346. if value is None:
  347. return Decimal('0')
  348. try:
  349. return Decimal(str(value))
  350. except (InvalidOperation, TypeError, ValueError):
  351. return Decimal('0')
  352. def _determine_detail_status(self, remaining_qty):
  353. if remaining_qty > 0:
  354. return 2, 'detail_should_be_in_stock'
  355. return 3, 'detail_should_be_outbound'
  356. def _status_display(self, status):
  357. status_map = {
  358. 0: '空盘',
  359. 2: '在盘',
  360. 3: '离库'
  361. }
  362. return status_map.get(status, str(status) if status is not None else '未知')
  363. def check_group_consistency(self):
  364. """检测库位组状态的一致性"""
  365. from bin.models import LocationGroupModel
  366. # 构建查询条件
  367. filters = {'is_active': True}
  368. if self.warehouse_code:
  369. filters['warehouse_code'] = self.warehouse_code
  370. if self.layer is not None:
  371. filters['layer'] = self.layer
  372. # 获取库位组并预取关联的库位
  373. groups = LocationGroupModel.objects.filter(**filters).prefetch_related('location_items')
  374. self.results['summary']['total_groups'] = groups.count()
  375. for group in groups:
  376. self.results['summary']['checked_groups'] += 1
  377. # 获取组内所有库位
  378. group_locations = group.location_items.filter(is_active=True)
  379. # 统计组内库位的状态分布
  380. status_counts = self._get_group_status_distribution(group_locations)
  381. total_locations = group_locations.count()
  382. # 根据组内库位状态推断组应该的状态
  383. is_consistent, expected_status, error_type = self._check_group_consistency(
  384. group.status, status_counts, total_locations
  385. )
  386. if not is_consistent:
  387. self.results['summary']['error_groups'] += 1
  388. self.results['group_errors'].append({
  389. 'group_id': group.id,
  390. 'group_code': group.group_code,
  391. 'group_name': group.group_name,
  392. 'warehouse_code': group.warehouse_code,
  393. 'current_status': group.status,
  394. 'expected_status': expected_status,
  395. 'total_locations': total_locations,
  396. 'status_distribution': status_counts,
  397. 'layer': group.layer,
  398. 'error_type': error_type,
  399. 'detected_at': timezone.now()
  400. })
  401. def _get_group_status_distribution(self, group_locations):
  402. """获取组内库位状态分布"""
  403. return group_locations.aggregate(
  404. available_count=Count('id', filter=Q(status='available')),
  405. occupied_count=Count('id', filter=Q(status='occupied')),
  406. disabled_count=Count('id', filter=Q(status='disabled')),
  407. reserved_count=Count('id', filter=Q(status='reserved')),
  408. maintenance_count=Count('id', filter=Q(status='maintenance'))
  409. )
  410. def _check_group_consistency(self, current_status, status_counts, total_locations):
  411. """
  412. 检查库位组状态一致性
  413. Returns:
  414. tuple: (是否一致, 预期状态, 错误类型)
  415. """
  416. if total_locations == 0:
  417. # 空组应该是available或disabled
  418. if current_status not in ['available', 'disabled']:
  419. return False, 'available', 'empty_group_wrong_status'
  420. else:
  421. # 根据库位状态分布推断组状态
  422. if status_counts['occupied_count'] == total_locations:
  423. # 所有库位都被占用,组状态应该是full
  424. if current_status != 'full':
  425. return False, 'full', 'all_occupied_but_not_full'
  426. elif status_counts['occupied_count'] > 0:
  427. # 有库位被占用,组状态应该是occupied
  428. if current_status != 'occupied':
  429. return False, 'occupied', 'has_occupied_but_wrong_status'
  430. elif status_counts['available_count'] == total_locations:
  431. # 所有库位都可用,组状态应该是available
  432. if current_status != 'available':
  433. return False, 'available', 'all_available_but_wrong_status'
  434. # 检查特殊状态
  435. elif status_counts['disabled_count'] > 0 and current_status != 'disabled':
  436. return False, 'disabled', 'has_disabled_but_wrong_status'
  437. elif status_counts['maintenance_count'] > 0 and current_status != 'maintenance':
  438. return False, 'maintenance', 'has_maintenance_but_wrong_status'
  439. return True, current_status, None
  440. def fix_detected_issues(self):
  441. """修复检测到的不一致问题"""
  442. logger.info("开始修复检测到的不一致问题")
  443. # 修复库位状态不一致
  444. if 'locations' in self.fix_scope and self.results['location_errors']:
  445. self._fix_location_issues()
  446. # 修复托盘明细状态不一致
  447. if 'details' in self.fix_scope and self.results['detail_errors']:
  448. self._fix_detail_issues()
  449. # 修复库位组状态不一致
  450. if 'groups' in self.fix_scope and self.results['group_errors']:
  451. self._fix_group_issues()
  452. logger.info(f"修复完成: {self.results['summary']['fixed_location_count']}个库位, "
  453. f"{self.results['summary']['fixed_group_count']}个库位组, "
  454. f"{self.results['summary']['fixed_detail_count']}条托盘明细")
  455. def _fix_location_issues(self):
  456. """修复库位状态不一致问题"""
  457. from bin.models import LocationModel
  458. for error in self.results['location_errors']:
  459. try:
  460. with transaction.atomic():
  461. location = LocationModel.objects.select_for_update().get(id=error['location_id'])
  462. # 只有明确有预期状态时才修复
  463. if error['expected_status'] and error['expected_status'] != 'need_check':
  464. old_status = location.status
  465. location.status = error['expected_status']
  466. location.save()
  467. self.results['summary']['fixed_location_count'] += 1
  468. self.results['fixed_locations'].append({
  469. 'location_id': location.id,
  470. 'location_code': location.location_code,
  471. 'old_status': old_status,
  472. 'new_status': location.status,
  473. 'fixed_at': timezone.now(),
  474. 'error_type': error['error_type']
  475. })
  476. else:
  477. # 标记为需要手动检查
  478. self.results['fixed_locations'].append({
  479. 'location_id': location.id,
  480. 'location_code': location.location_code,
  481. 'status': '需要手动检查',
  482. 'reason': '无法自动确定正确状态',
  483. 'error_type': error['error_type']
  484. })
  485. except Exception as e:
  486. logger.error(f"修复库位{error.get('location_id')}时出错: {str(e)}")
  487. self.results['repair_errors'].append({
  488. 'type': 'location_fix_error',
  489. 'location_id': error.get('location_id'),
  490. 'error_message': str(e),
  491. 'error_type': error.get('error_type')
  492. })
  493. def _fix_group_issues(self):
  494. """修复库位组状态不一致问题"""
  495. from bin.models import LocationGroupModel
  496. for error in self.results['group_errors']:
  497. try:
  498. with transaction.atomic():
  499. group = LocationGroupModel.objects.select_for_update().get(id=error['group_id'])
  500. # 只有明确有预期状态时才修复
  501. if error['expected_status'] and error['expected_status'] != 'need_check':
  502. old_status = group.status
  503. group.status = error['expected_status']
  504. group.save()
  505. self.results['summary']['fixed_group_count'] += 1
  506. self.results['fixed_groups'].append({
  507. 'group_id': group.id,
  508. 'group_code': group.group_code,
  509. 'old_status': old_status,
  510. 'new_status': group.status,
  511. 'fixed_at': timezone.now(),
  512. 'error_type': error['error_type']
  513. })
  514. else:
  515. # 标记为需要手动检查
  516. self.results['fixed_groups'].append({
  517. 'group_id': group.id,
  518. 'group_code': group.group_code,
  519. 'status': '需要手动检查',
  520. 'reason': '无法自动确定正确状态',
  521. 'error_type': error.get('error_type')
  522. })
  523. except Exception as e:
  524. logger.error(f"修复库位组{error.get('group_id')}时出错: {str(e)}")
  525. self.results['repair_errors'].append({
  526. 'type': 'group_fix_error',
  527. 'group_id': error.get('group_id'),
  528. 'error_message': str(e),
  529. 'error_type': error.get('error_type')
  530. })
  531. def _fix_detail_issues(self):
  532. """修复托盘明细状态不一致问题"""
  533. from container.models import ContainerDetailModel
  534. for error in self.results['detail_errors']:
  535. expected_status = error.get('expected_status')
  536. if expected_status not in [2, 3]:
  537. continue
  538. try:
  539. with transaction.atomic():
  540. detail = ContainerDetailModel.objects.select_for_update().select_related('container').get(id=error['detail_id'])
  541. if detail.status == expected_status:
  542. continue
  543. old_status = detail.status
  544. detail.status = expected_status
  545. detail.save(update_fields=['status'])
  546. self.results['summary']['fixed_detail_count'] += 1
  547. error['fixed'] = True
  548. error['fixed_at'] = timezone.now()
  549. error['new_status'] = expected_status
  550. error['new_status_display'] = self._status_display(expected_status)
  551. self.results['fixed_details'].append({
  552. 'detail_id': detail.id,
  553. 'container_code': getattr(detail.container, 'container_code', None),
  554. 'old_status': old_status,
  555. 'new_status': expected_status,
  556. 'fixed_at': timezone.now(),
  557. 'error_type': error.get('error_type')
  558. })
  559. except ContainerDetailModel.DoesNotExist:
  560. logger.warning(f"托盘明细 {error.get('detail_id')} 不存在,跳过修复")
  561. except Exception as e:
  562. logger.error(f"修复托盘明细{error.get('detail_id')}失败: {str(e)}")
  563. self.results['repair_errors'].append({
  564. 'type': 'detail_fix_error',
  565. 'detail_id': error.get('detail_id'),
  566. 'error_message': str(e),
  567. 'error_type': error.get('error_type')
  568. })
  569. def get_summary(self):
  570. """获取检测摘要"""
  571. return {
  572. 'check_time': self.results['check_time'],
  573. 'total_checked': {
  574. 'locations': self.results['summary']['checked_locations'],
  575. 'groups': self.results['summary']['checked_groups'],
  576. 'details': self.results['summary']['checked_details']
  577. },
  578. 'errors_found': {
  579. 'locations': self.results['summary']['error_locations'],
  580. 'groups': self.results['summary']['error_groups'],
  581. 'details': self.results['summary']['error_details']
  582. },
  583. 'fixed': {
  584. 'locations': self.results['summary']['fixed_location_count'],
  585. 'groups': self.results['summary']['fixed_group_count'],
  586. 'details': self.results['summary']['fixed_detail_count']
  587. },
  588. 'has_errors': len(self.results['repair_errors']) > 0
  589. }
  590. def generate_report(self):
  591. """生成检测报告"""
  592. summary = self.get_summary()
  593. report = {
  594. 'summary': summary,
  595. 'details': {
  596. 'location_errors': self.results['location_errors'],
  597. 'group_errors': self.results['group_errors'],
  598. 'detail_errors': self.results['detail_errors'],
  599. 'fixed_locations': self.results['fixed_locations'],
  600. 'fixed_groups': self.results['fixed_groups'],
  601. 'fixed_details': self.results['fixed_details'],
  602. 'repair_errors': self.results['repair_errors']
  603. }
  604. }
  605. return report
  606. # 便捷函数
  607. def check_location_consistency(warehouse_code=None, layer=None, auto_fix=False):
  608. """
  609. 便捷函数:检查库位一致性
  610. Args:
  611. warehouse_code: 仓库代码
  612. layer: 楼层
  613. auto_fix: 是否自动修复
  614. Returns:
  615. dict: 检测结果
  616. """
  617. checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
  618. return checker.check_all()
  619. def get_consistency_report(warehouse_code=None, layer=None, auto_fix=False):
  620. """
  621. 获取详细的检测报告
  622. Args:
  623. warehouse_code: 仓库代码
  624. layer: 楼层
  625. auto_fix: 是否自动修复
  626. Returns:
  627. dict: 检测报告
  628. """
  629. checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
  630. checker.check_all()
  631. return checker.generate_report()