models.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. from django.db import models
  2. from bound.models import BoundBatchModel,BoundDetailModel,OutBatchModel,BoundListModel,BoundListModel,MaterialStatistics
  3. from django.utils import timezone
  4. from django.db.models.signals import pre_save, post_save
  5. from django.dispatch import receiver
  6. from decimal import Decimal
  7. from django.db.models import Sum
  8. from datetime import timedelta
  9. from django.db.models import Q
  10. from django.db.models import Sum, F
  11. import logging
  12. logger = logging.getLogger(__name__)
  13. import threading
  14. from django.db import close_old_connections
  15. from concurrent.futures import ThreadPoolExecutor
  16. import queue
  17. # 创建线程池执行器和任务队列
  18. log_processing_executor = ThreadPoolExecutor(max_workers=4)
  19. log_processing_queue = queue.Queue()
  20. def async_worker():
  21. """后台线程处理日志任务"""
  22. while True:
  23. try:
  24. # 从队列获取任务
  25. logs, created = log_processing_queue.get(timeout=30)
  26. close_old_connections() # 确保每次处理前关闭旧连接
  27. # 处理批量日志
  28. processed_logs = []
  29. for log in logs:
  30. try:
  31. aggregate_to_batch_log(log)
  32. count_day_in_out(log)
  33. processed_logs.append(log.id)
  34. except Exception as e:
  35. logger.error(f"处理日志时出错: {e}", exc_info=True)
  36. # 如果有批次日志需要触发material history更新
  37. if created:
  38. from bound.models import MaterialStatistics
  39. # 这里可以添加批量更新MaterialStatistics的逻辑
  40. # ...
  41. logger.info(f"已处理 {len(processed_logs)} 条日志")
  42. log_processing_queue.task_done()
  43. except queue.Empty:
  44. # 30秒无任务则进入休眠
  45. continue
  46. except Exception as e:
  47. logger.error(f"后台线程异常: {e}", exc_info=True)
  48. # 启动后台线程处理日志
  49. threading.Thread(target=async_worker, daemon=True).start()
  50. # Create your models here.
  51. # 主表:托盘数据
  52. class ContainerListModel(models.Model):
  53. CONTAINER_STATUS = (
  54. (0, '空置'),
  55. (1, '入库中'),
  56. (2, '在库'),
  57. (3, '出库中'),
  58. (4, '已出库'),
  59. (5, '空托盘组')
  60. )
  61. # 新增托盘分类字段
  62. CONTAINER_CATEGORY_CHOICES = (
  63. (0, '整盘'),
  64. (1, '散盘'),
  65. (2, '空盘在库'),
  66. (3, '空盘不在库'),
  67. )
  68. container_code = models.IntegerField( verbose_name='托盘编号',unique=True)
  69. current_location = models.CharField(max_length=50, verbose_name='当前库位', default='N/A')
  70. target_location = models.CharField(max_length=50, verbose_name='目标库位', default='N/A')
  71. available= models.BooleanField(default=True, verbose_name='可用')
  72. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='托盘状态')
  73. last_operation = models.DateTimeField(auto_now=True, verbose_name='最后操作时间')
  74. category = models.IntegerField(
  75. choices=CONTAINER_CATEGORY_CHOICES,
  76. default=3, # 默认空盘不在库
  77. verbose_name='托盘分类'
  78. )
  79. # 新增批次数组字段(缓存)
  80. batch_info = models.JSONField(
  81. default=list,
  82. verbose_name='批次信息缓存',
  83. help_text='格式: [{"batch_id":1,"check_status":"合格","bound_number":"B001","qty":100}]'
  84. )
  85. class Meta:
  86. db_table = 'container_list'
  87. verbose_name = 'ContainerList'
  88. verbose_name_plural = "ContainerList"
  89. ordering = ['-container_code']
  90. class ContainerDetailModel(models.Model):
  91. BATCH_STATUS=(
  92. (0, '空盘'),
  93. (2, '在盘'),
  94. (3, '离库')
  95. )
  96. BATCH_CLASS = (
  97. (1, '成品'),
  98. (2, '空盘'),
  99. (3, '散盘'),
  100. )
  101. month = models.IntegerField(verbose_name='月份')
  102. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='details')
  103. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',blank=True, null=True)
  104. goods_class = models.IntegerField(verbose_name='货品类别',choices=BATCH_CLASS, default=1)
  105. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  106. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  107. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name='数量')
  108. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='出库数量')
  109. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  110. status = models.IntegerField(choices=BATCH_STATUS,default=0, verbose_name='状态')
  111. creater = models.CharField(max_length=50, verbose_name='创建人')
  112. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  113. update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
  114. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  115. class Meta:
  116. db_table = 'container_detail'
  117. verbose_name = 'ContainerDetail'
  118. verbose_name_plural = "ContainerDetail"
  119. ordering = ['-id']
  120. indexes = [
  121. models.Index(fields=['container']),
  122. models.Index(fields=['batch']),
  123. models.Index(fields=['goods_code']),
  124. models.Index(fields=['status']),
  125. ]
  126. def __str__(self):
  127. return f"{self.container.container_code} - {self.batch.bound_number if self.batch else 'N/A'} - {self.goods_code}"
  128. def save(self, *args, **kwargs):
  129. """
  130. 更新托盘上的物料数量,更新批次上的
  131. goods_in_qty(组盘数目)
  132. ,goods_in_location_qty(在库数目)
  133. ,goods_out_qty(出库数目)
  134. """
  135. # 在保存前调用信号处理器
  136. if self.pk:
  137. # 获取状态变化前的状态
  138. original = ContainerDetailModel.objects.get(pk=self.pk)
  139. original_status = original.status
  140. super().save(*args, **kwargs)
  141. # 更新批次数据
  142. if self.batch:
  143. # 避免循环更新 - 仅在数量相关字段变更时才更新
  144. if 'update_fields' not in kwargs or any(field in kwargs['update_fields'] for field in ['goods_qty', 'goods_out_qty','is_delete']):
  145. self.update_batch_stats()
  146. # # 根据出库数量更新状态
  147. # if self.goods_out_qty > self.goods_qty:
  148. # # 出库数量不能大于总数量
  149. # self.goods_out_qty = self.goods_qty
  150. # self.save(update_fields=['goods_out_qty'])
  151. # 根据当前数量状态更新状态
  152. if self.goods_out_qty >= self.goods_qty:
  153. self.status = 3 # 已出库
  154. super().save(*args, **kwargs)
  155. elif self.goods_qty - self.goods_out_qty > 0 and self.goods_out_qty > 0:
  156. self.status = 2 # 在库
  157. super().save(*args, **kwargs)
  158. if self.status == 3 and self.goods_qty - self.goods_out_qty > 0 :
  159. self.status = 2 # 在库
  160. super().save(*args, **kwargs)
  161. # 更新货物分类(关键修改点)
  162. if not kwargs.get('skip_class_update', False):
  163. # 获取托盘上所有物料(排除已删除和已出库的)
  164. details = ContainerDetailModel.objects.filter(
  165. container=self.container.id,
  166. is_delete=False
  167. ).exclude(status=3)
  168. # 统计不同批次数目
  169. batch_count = details.values('batch').distinct().count()
  170. new_class = 1 if batch_count == 1 else 3
  171. # 批量更新所有相关物料的goods_class
  172. details.exclude(goods_class=new_class).update(goods_class=new_class)
  173. # 如果当前物料的class需要更新
  174. if self.goods_class != new_class:
  175. self.goods_class = new_class
  176. super().save(update_fields=['goods_class'])
  177. def update_batch_stats(self):
  178. """更新批次的统计数据"""
  179. if not self.batch:
  180. return
  181. # 聚合托盘明细数据
  182. stats = ContainerDetailModel.objects.filter(
  183. batch=self.batch,
  184. is_delete=False
  185. ).aggregate(
  186. total_qty=models.Sum('goods_qty'),
  187. total_out_qty=models.Sum('goods_out_qty')
  188. )
  189. # 更新批次数据
  190. self.batch.goods_in_qty = stats['total_qty'] or 0
  191. self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0)
  192. self.batch.goods_out_qty = stats['total_out_qty'] or 0
  193. self.batch.save()
  194. def get_container_code(self):
  195. return self.container.container_code
  196. class ContainerDetailLogModel(models.Model):
  197. """托盘明细变更日志模型"""
  198. LOG_TYPES = (
  199. ('create', '创建'),
  200. ('update', '更新'),
  201. ('delete', '删除'),
  202. ('out', '出库'),
  203. ('cancel_out', '取消出库'),
  204. ('status_change', '状态变更'),
  205. )
  206. # 关联的托盘明细
  207. container_detail = models.ForeignKey(
  208. ContainerDetailModel,
  209. on_delete=models.CASCADE,
  210. related_name='logs'
  211. )
  212. # 日志类型
  213. log_type = models.CharField(
  214. max_length=20,
  215. choices=LOG_TYPES,
  216. verbose_name='日志类型'
  217. )
  218. # 原值
  219. old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
  220. old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True)
  221. old_status = models.IntegerField(
  222. choices=ContainerDetailModel.BATCH_STATUS,
  223. null=True,
  224. blank=True,
  225. verbose_name='原状态'
  226. )
  227. # 新值
  228. new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
  229. new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True)
  230. new_status = models.IntegerField(
  231. choices=ContainerDetailModel.BATCH_STATUS,
  232. null=True,
  233. blank=True,
  234. verbose_name='新状态'
  235. )
  236. # 元信息
  237. creater = models.CharField(max_length=50, verbose_name='操作人')
  238. create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  239. tobatchlog = models.BooleanField(default=False, verbose_name='是否转移到批次日志')
  240. class Meta:
  241. db_table = 'container_detail_log'
  242. verbose_name = '托盘明细变更日志'
  243. verbose_name_plural = "托盘明细变更日志"
  244. ordering = ['-create_time']
  245. def __str__(self):
  246. return f"{self.container_detail} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  247. class batchLogModel(models.Model):
  248. """批次日志模型"""
  249. LOG_TYPES = (
  250. ('create', '创建'),
  251. ('update', '更新'),
  252. ('delete', '删除'),
  253. ('out', '出库'),
  254. ('cancel_out', '取消出库'),
  255. ('status_change', '状态变更'),
  256. )
  257. bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='logs', null=True, blank=True)
  258. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, related_name='logs')
  259. log_type = models.CharField(max_length=20, choices=LOG_TYPES, verbose_name='日志类型')
  260. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  261. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  262. goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
  263. goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
  264. goods_in_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新入数量', null=True, blank=True)
  265. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出数量', null=True, blank=True)
  266. detail_logs = models.ManyToManyField(
  267. ContainerDetailLogModel,
  268. related_name='batch_logs',
  269. verbose_name='关联托盘日志'
  270. )
  271. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  272. class Meta:
  273. db_table = 'batch_log_from_container_log'
  274. verbose_name = '批次日志'
  275. verbose_name_plural = "批次日志"
  276. ordering = ['-create_time']
  277. def __str__(self):
  278. return f"{self.batch} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  279. def update_from_details(self):
  280. """从关联的托盘日志更新批次日志数据"""
  281. # 获取所有关联的托盘日志
  282. detail_logs = self.detail_logs.all()
  283. # 计算聚合值
  284. total_old_qty = detail_logs.aggregate(total=Sum('old_goods_qty'))['total'] or 0
  285. total_new_qty = detail_logs.aggregate(total=Sum('new_goods_qty'))['total'] or 0
  286. total_old_out = detail_logs.aggregate(total=Sum('old_goods_out_qty'))['total'] or 0
  287. total_new_out = detail_logs.aggregate(total=Sum('new_goods_out_qty'))['total'] or 0
  288. # 更新批次日志
  289. self.goods_in_qty = total_new_qty - total_old_qty
  290. self.goods_out_qty = total_new_out - total_old_out
  291. self.save()
  292. # 同时更新批次统计数据
  293. self.update_batch_stats()
  294. def update_batch_stats(self):
  295. """更新批次的统计数据"""
  296. # 聚合托盘明细数据
  297. stats = ContainerDetailModel.objects.filter(
  298. batch=self.batch,
  299. is_delete=False
  300. ).aggregate(
  301. total_qty=Sum('goods_qty'),
  302. total_out_qty=Sum('goods_out_qty')
  303. )
  304. # 更新批次数据
  305. self.batch.goods_in_qty = stats['total_qty'] or 0
  306. self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0)
  307. self.batch.goods_out_qty = stats['total_out_qty'] or 0
  308. self.batch.save()
  309. # 批次日志聚合处理器
  310. def aggregate_to_batch_log(container_log):
  311. """将托盘日志聚合到批次日志"""
  312. logger.info(f"开始聚合托盘日志: {container_log.id}")
  313. # 检查日志是否已处理
  314. if container_log.tobatchlog:
  315. logger.info(f"托盘日志 {container_log.id} 已处理过,跳过")
  316. return
  317. try:
  318. # 确定批次
  319. detail = container_log.container_detail
  320. if not detail:
  321. logger.warning(f"托盘日志 {container_log.id} 缺少关联的托盘明细")
  322. return
  323. batch = detail.batch
  324. if not batch:
  325. logger.warning(f"托盘明细 {detail.id} 缺少关联的批次")
  326. return
  327. # 确定操作类型
  328. log_type = container_log.log_type
  329. # 创建或获取批次日志(按类型和时间窗口分组)
  330. # 设置时间窗口(5分钟)
  331. time_window_start = container_log.create_time - timedelta(minutes=2)
  332. time_window_end = container_log.create_time + timedelta(minutes=2)
  333. # 查找相同批次、相同类型、在时间窗口内的批次日志
  334. existing_logs = batchLogModel.objects.filter(
  335. batch=batch,
  336. log_type=log_type,
  337. create_time__gte=time_window_start,
  338. create_time__lte=time_window_end
  339. ).order_by('create_time')
  340. if existing_logs.exists():
  341. # 使用时间窗口内最早的批次日志
  342. batch_log = existing_logs.first()
  343. logger.info(f"找到已有批次日志 {batch_log.id} 用于聚合")
  344. else:
  345. # 创建新的批次日志
  346. batch_log = batchLogModel.objects.create(
  347. batch=batch,
  348. log_type=log_type,
  349. goods_code=batch.goods_code,
  350. goods_desc=batch.goods_desc,
  351. goods_std=batch.goods_std,
  352. goods_unit=batch.goods_unit,
  353. create_time=container_log.create_time
  354. )
  355. logger.info(f"创建新批次日志 {batch_log.id}")
  356. # create_material_history(batch_log, update=True)
  357. # 将托盘日志关联到批次日志
  358. # 先检查记录是否仍然存在(防止在处理过程中被删除)
  359. if not ContainerDetailLogModel.objects.filter(id=container_log.id).exists():
  360. logger.warning(f"托盘日志 {container_log.id} 已不存在,跳过关联(可能已被级联删除)")
  361. return
  362. batch_log.detail_logs.add(container_log)
  363. # 从关联日志更新批次日志数据
  364. batch_log.update_from_details()
  365. # 根据日志类型添加关联单据信息
  366. if log_type == 'out' and not batch_log.bound:
  367. from bound.models import OutBoundDetailModel
  368. bound = OutBoundDetailModel.objects.filter(
  369. bound_batch_number=batch
  370. ).order_by('-create_time').first()
  371. if bound:
  372. batch_log.bound = bound.bound_list
  373. batch_log.save()
  374. logger.info(f"为批次日志 {batch_log.id} 添加出库单关联")
  375. if log_type == 'create' and not batch_log.bound:
  376. from bound.models import BoundDetailModel
  377. bound = BoundDetailModel.objects.filter(
  378. bound_batch=batch
  379. ).order_by('-create_time').first()
  380. if bound:
  381. batch_log.bound = bound.bound_list
  382. logger.info(f"为批次日志 {batch_log.id} 添加入库单关联")
  383. batch_log.save()
  384. # 标记日志已处理
  385. # 再次检查记录是否仍然存在(防止在处理过程中被删除)
  386. if ContainerDetailLogModel.objects.filter(id=container_log.id).exists():
  387. container_log.tobatchlog = True
  388. container_log.save()
  389. logger.info(f"成功聚合托盘日志 {container_log.id} 到批次日志 {batch_log.id}")
  390. else:
  391. logger.warning(f"托盘日志 {container_log.id} 在标记处理状态时已不存在,跳过保存")
  392. return batch_log
  393. except Exception as e:
  394. logger.error(f"聚合托盘日志时出错: {e}", exc_info=True)
  395. raise
  396. # # 批次日志的信号处理器
  397. @receiver(post_save, sender=batchLogModel)
  398. def update_material_history(sender, instance, created, **kwargs):
  399. """批次日志保存后更新物料统计"""
  400. if created:
  401. # 创建物料变动历史记录
  402. create_material_history(instance, update=True)
  403. def create_material_history( instance, update):
  404. """为每条批次日志创建物料变动历史记录"""
  405. from bound.models import MaterialStatistics
  406. if update:
  407. # 获取或创建物料统计
  408. stats, _ = MaterialStatistics.objects.get_or_create(
  409. goods_code=instance.goods_code,
  410. defaults={
  411. 'goods_desc': instance.goods_desc,
  412. 'goods_std': instance.goods_std or '待填写',
  413. 'goods_unit': instance.goods_unit or '待填写',
  414. }
  415. )
  416. # 计算期初数量(变动前的库存)
  417. opening_quantity = stats.total_quantity
  418. # 计算期末数量
  419. closing_quantity = opening_quantity
  420. if instance.goods_in_qty:
  421. closing_quantity += instance.goods_in_qty
  422. if instance.goods_out_qty:
  423. closing_quantity -= instance.goods_out_qty
  424. MaterialChangeHistory.objects.create(
  425. material_stats=stats,
  426. batch_log=instance,
  427. goods_code=instance.goods_code,
  428. goods_desc=instance.goods_desc,
  429. goods_std=instance.goods_std,
  430. goods_unit=instance.goods_unit,
  431. change_time=instance.create_time,
  432. in_quantity=instance.goods_in_qty or Decimal('0'),
  433. out_quantity=instance.goods_out_qty or Decimal('0'),
  434. change_type=instance.log_type,
  435. opening_quantity=opening_quantity,
  436. closing_quantity=closing_quantity
  437. )
  438. # 更新物料统计的实时库存
  439. stats.total_quantity = closing_quantity
  440. stats.save()
  441. def count_day_in_out(ContainerDetailLogModelobj):
  442. """统计某批次的每日入库和出库数量"""
  443. from reportcenter.models import bigScreenModel
  444. from datetime import datetime, timedelta
  445. count_today, _ = bigScreenModel.objects.get_or_create(
  446. day = datetime.now().date(),
  447. defaults={
  448. 'day_in': 0,
  449. 'day_out': 0,
  450. }
  451. )
  452. count_today.day_in += ContainerDetailLogModelobj.new_goods_qty - ContainerDetailLogModelobj.old_goods_qty or 0
  453. count_today.day_out += ContainerDetailLogModelobj.new_goods_out_qty - ContainerDetailLogModelobj.old_goods_out_qty or 0
  454. count_today.save()
  455. # 获取批次的创建时间
  456. @receiver(pre_save, sender=ContainerDetailModel)
  457. def container_detail_pre_save(sender, instance, **kwargs):
  458. """在托盘明细保存前记录变更"""
  459. if instance.pk:
  460. try:
  461. old_instance = ContainerDetailModel.objects.get(pk=instance.pk)
  462. logs = []
  463. # 数量变化日志
  464. if old_instance.goods_qty != instance.goods_qty:
  465. logs.append(ContainerDetailLogModel(
  466. container_detail=instance,
  467. log_type='update',
  468. old_goods_qty=old_instance.goods_qty,
  469. new_goods_qty=instance.goods_qty,
  470. creater=instance.creater
  471. ))
  472. # 出库数量变化日志
  473. if old_instance.goods_out_qty != instance.goods_out_qty:
  474. if old_instance.goods_out_qty < instance.goods_out_qty:
  475. log_type = 'out'
  476. else:
  477. log_type = 'cancel_out'
  478. logs.append(ContainerDetailLogModel(
  479. container_detail=instance,
  480. log_type=log_type,
  481. old_goods_out_qty=old_instance.goods_out_qty,
  482. new_goods_out_qty=instance.goods_out_qty,
  483. creater=instance.creater
  484. ))
  485. # 删除日志
  486. if old_instance.is_delete != instance.is_delete and instance.is_delete:
  487. logs.append(ContainerDetailLogModel(
  488. container_detail=instance,
  489. log_type='delete',
  490. old_goods_qty=old_instance.goods_qty,
  491. old_goods_out_qty=old_instance.goods_out_qty,
  492. new_goods_qty = old_instance.goods_qty,
  493. new_goods_out_qty = old_instance.goods_qty,
  494. old_status=old_instance.status,
  495. creater=instance.creater
  496. ))
  497. # 批量创建日志
  498. if logs:
  499. created_logs = ContainerDetailLogModel.objects.bulk_create(logs)
  500. # print(f"创建{len(created_logs)}条ContainerDetailLogModel日志")
  501. for log in created_logs:
  502. # 由于bulk_create不会触发信号,我们手动调用信号处理函数
  503. handle_container_detail_log(ContainerDetailLogModel, log, created=True)
  504. except ContainerDetailModel.DoesNotExist:
  505. pass
  506. @receiver(post_save, sender=ContainerDetailLogModel)
  507. def handle_container_detail_log(sender, instance, created, **kwargs):
  508. """将日志放入队列进行后台处理"""
  509. if created:
  510. # 检查实例是否已处理过(防止多次入队)
  511. if hasattr(instance, '_is_enqueued'):
  512. return
  513. instance._is_enqueued = True
  514. try:
  515. log_processing_queue.put(([instance], created), block=False)
  516. logger.info(f"已放入队列处理: {instance.id}")
  517. except queue.Full:
  518. print("日志处理队列已满,将同步处理")
  519. logger.warning("日志处理队列已满,将同步处理")
  520. # 队列满时同步处理当前日志
  521. aggregate_to_batch_log(instance)
  522. count_day_in_out(instance)
  523. # if created:
  524. # # 创建物料变动历史记录
  525. # create_material_history(instance, update=True)
  526. @receiver(post_save, sender=ContainerDetailModel)
  527. def container_detail_post_save(sender, instance, created, **kwargs):
  528. """在托盘明细新建时创建日志"""
  529. if created:
  530. ContainerDetailLogModel.objects.create(
  531. container_detail=instance,
  532. log_type='create',
  533. creater=instance.creater,
  534. new_goods_qty=instance.goods_qty,
  535. new_status=instance.status
  536. )
  537. class MaterialChangeHistory(models.Model):
  538. """物料库存变动历史记录(与批次日志多对一)"""
  539. material_stats = models.ForeignKey(
  540. MaterialStatistics,
  541. on_delete=models.CASCADE,
  542. related_name='history_records',
  543. verbose_name="关联物料"
  544. )
  545. # 与批次日志建立多对一关系
  546. batch_log = models.ForeignKey(batchLogModel,on_delete=models.CASCADE, related_name='material_history', verbose_name="关联批次日志",primary_key=False)
  547. count_time = models.IntegerField(verbose_name='统计次数',default=0,null=True, blank=True)
  548. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  549. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  550. goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
  551. goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
  552. # 时间戳记录(使用批次日志的时间)
  553. change_time = models.DateTimeField(
  554. verbose_name="变动时间"
  555. )
  556. # 库存变动情况
  557. in_quantity = models.DecimalField(
  558. max_digits=10, decimal_places=3,
  559. default=Decimal('0'),
  560. verbose_name="入库数量"
  561. )
  562. out_quantity = models.DecimalField(
  563. max_digits=10, decimal_places=3,
  564. default=Decimal('0'),
  565. verbose_name="出库数量"
  566. )
  567. # 变更类型(从批次日志获取)
  568. change_type = models.CharField(
  569. max_length=20,
  570. verbose_name="变动类型"
  571. )
  572. # 变更时的库存快照
  573. opening_quantity = models.DecimalField(
  574. max_digits=10, decimal_places=3,
  575. default=Decimal('0'),
  576. verbose_name="期初数量"
  577. )
  578. closing_quantity = models.DecimalField(
  579. max_digits=10, decimal_places=3,
  580. default=Decimal('0'),
  581. verbose_name="期末数量"
  582. )
  583. class Meta:
  584. db_table = 'material_change_history'
  585. verbose_name = '物料变动历史'
  586. verbose_name_plural = "物料变动历史"
  587. ordering = ['-change_time']
  588. indexes = [
  589. models.Index(fields=['material_stats', 'change_time']),
  590. models.Index(fields=['change_time']),
  591. ]
  592. # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录
  593. class ContainerOperationModel(models.Model):
  594. OPERATION_TYPES = (
  595. ('container','组盘'),
  596. ('inbound', '入库'),
  597. ('outbound', '出库'),
  598. ('adjust', '调整'),
  599. )
  600. month = models.IntegerField(verbose_name='月份')
  601. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations')
  602. operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型')
  603. bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True)
  604. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',null=True, blank=True)
  605. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  606. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  607. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  608. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  609. operator = models.CharField(max_length=50, verbose_name='操作人')
  610. timestamp = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  611. from_location = models.CharField(max_length=50, null=True, verbose_name='原库位')
  612. to_location = models.CharField(max_length=50, null=True, verbose_name='目标库位')
  613. memo = models.TextField(null=True, verbose_name='备注')
  614. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  615. class Meta:
  616. db_table = 'container_operation'
  617. verbose_name = 'ContainerOperation'
  618. verbose_name_plural = "ContainerOperation"
  619. ordering = ['-timestamp']
  620. class ContainerWCSModel(models.Model):
  621. TASK_STATUS = (
  622. (100, '等待中'),
  623. (101, '处理中'),
  624. (102, '已暂停'),
  625. (103, '入库中'),
  626. (200, '已发送'),
  627. (300, '已完成'),
  628. (400, '失败'),
  629. )
  630. taskid = models.CharField(max_length=50, verbose_name='任务ID')
  631. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次',null=True, blank=True )
  632. batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True )
  633. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单',null=True, blank=True )
  634. batch_number = models.CharField(max_length=50, verbose_name='批次号',null=True, blank=True )
  635. sequence = models.BigIntegerField(verbose_name='任务顺序')
  636. priority = models.IntegerField(default=100, verbose_name='优先级')
  637. month = models.IntegerField(verbose_name='月份')
  638. tasktype = models.CharField(max_length=50, verbose_name='任务类型')
  639. tasknumber = models.BigIntegerField(verbose_name='任务号',unique=True)
  640. order_number = models.IntegerField(verbose_name='c_number')
  641. container = models.CharField(max_length=50, verbose_name='托盘号')
  642. current_location = models.CharField(max_length=50, verbose_name='当前库位')
  643. target_location = models.CharField(max_length=50, verbose_name='目标库位')
  644. message = models.TextField(verbose_name='消息')
  645. working = models.IntegerField(default = 1,verbose_name='工作状态')
  646. status = models.IntegerField(choices=TASK_STATUS, default=100, verbose_name='状态')
  647. location_group_id = models.IntegerField(null=True, blank=True, verbose_name='库位组ID')
  648. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  649. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  650. class Meta:
  651. db_table = 'container_wcs'
  652. verbose_name = 'ContainerWCS'
  653. verbose_name_plural = "ContainerWCS"
  654. ordering = ['-create_time']
  655. def to_dict(self):
  656. return {
  657. 'container': self.container,
  658. 'current_location': self.current_location,
  659. 'month' : self.month,
  660. 'target_location': self.target_location,
  661. 'tasktype': self.tasktype,
  662. 'taskid': self.taskid,
  663. 'taskNumber': self.tasknumber-20000000000,
  664. 'message': self.message,
  665. 'container': self.container,
  666. 'status': self.status,
  667. 'location_group_id': self.location_group_id,
  668. 'batch_number': self.batch_number,
  669. 'bound_list_id': self.bound_list_id,
  670. }
  671. def __str__(self):
  672. return f"{self.taskid} - {self.get_status_display()}"
  673. # 这里的批次详情是主入库申请单下的子批次
  674. class TaskModel(models.Model):
  675. task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, related_name='tasks')
  676. batch_detail = models.ForeignKey(BoundDetailModel, on_delete=models.CASCADE, verbose_name='批次详情')
  677. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  678. class Meta:
  679. db_table = 'task'
  680. verbose_name = 'Task'
  681. verbose_name_plural = "Task"
  682. ordering = ['-id']
  683. class out_batch_detail(models.Model):
  684. out_bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  685. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  686. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  687. out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  688. last_out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='上次出库数量')
  689. working = models.IntegerField(default = 1,verbose_name='工作状态')
  690. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  691. class Meta:
  692. db_table = 'out_batch_detail'
  693. verbose_name = 'OutBatchDetail'
  694. verbose_name_plural = "OutBatchDetail"
  695. ordering = ['container']
  696. class DispatchConfig(models.Model):
  697. """
  698. 任务下发调度配置
  699. - cross_floor_concurrent_limit: 跨楼层并发下发上限(不同楼层同时下发的任务数)
  700. - intra_floor_order: 同层排序策略(当前仅支持:batch_then_sequence)
  701. - enabled: 是否启用
  702. """
  703. INTRA_FLOOR_ORDER_CHOICES = (
  704. ('batch_then_sequence', '按批次优先,再按顺序'),
  705. )
  706. cross_floor_concurrent_limit = models.PositiveIntegerField(
  707. default=2,
  708. verbose_name='跨楼层并发上限'
  709. )
  710. intra_floor_order = models.CharField(
  711. max_length=64,
  712. choices=INTRA_FLOOR_ORDER_CHOICES,
  713. default='batch_then_sequence',
  714. verbose_name='同层排序策略'
  715. )
  716. enabled = models.BooleanField(default=True, verbose_name='是否启用')
  717. update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
  718. class Meta:
  719. db_table = 'dispatch_config'
  720. verbose_name = '任务下发配置'
  721. verbose_name_plural = "任务下发配置"
  722. ordering = ['-id']
  723. @classmethod
  724. def get_active_config(cls):
  725. config = cls.objects.filter(enabled=True).first()
  726. if not config:
  727. config = cls.objects.create()
  728. return config
  729. class WCSTaskLogModel(models.Model):
  730. """
  731. WCS 任务发送日志记录
  732. 每当系统发送任务到 WCS 时异步记录(不阻塞发送)
  733. """
  734. task_number = models.BigIntegerField(verbose_name='任务号')
  735. container = models.CharField(max_length=50, verbose_name='托盘号')
  736. current_location = models.CharField(max_length=100, verbose_name='起始位置')
  737. target_location = models.CharField(max_length=100, verbose_name='目标位置')
  738. location_group_id = models.IntegerField(null=True, blank=True, verbose_name='库位组ID')
  739. left_priority = models.IntegerField(null=True, blank=True, verbose_name='左侧优先级')
  740. right_priority = models.IntegerField(null=True, blank=True, verbose_name='右侧优先级')
  741. task_type = models.CharField(max_length=50, null=True, blank=True, verbose_name='任务类型')
  742. order_number = models.IntegerField(null=True, blank=True, verbose_name='订单号')
  743. sequence = models.IntegerField(null=True, blank=True, verbose_name='序列号')
  744. send_time = models.DateTimeField(auto_now_add=True, verbose_name='发送时间')
  745. response_data = models.JSONField(null=True, blank=True, verbose_name='WCS返回结果')
  746. floor = models.CharField(max_length=20, null=True, blank=True, verbose_name='楼层')
  747. is_completed = models.BooleanField(default=False, verbose_name='是否完成')
  748. log_type = models.CharField(max_length=32, default='outbound', verbose_name='日志类型')
  749. class Meta:
  750. db_table = 'wcs_task_log'
  751. verbose_name = 'WCS任务发送日志'
  752. verbose_name_plural = "WCS任务发送日志"
  753. ordering = ['-send_time']
  754. indexes = [
  755. models.Index(fields=['task_number']),
  756. models.Index(fields=['container']),
  757. models.Index(fields=['send_time']),
  758. ]