models.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  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. import logging
  11. logger = logging.getLogger(__name__)
  12. # Create your models here.
  13. # 主表:托盘数据
  14. class ContainerListModel(models.Model):
  15. CONTAINER_STATUS = (
  16. (0, '空置'),
  17. (1, '入库中'),
  18. (2, '在库'),
  19. (3, '出库中'),
  20. (4, '已出库'),
  21. (5, '空托盘组')
  22. )
  23. container_code = models.IntegerField( verbose_name='托盘编号',unique=True)
  24. current_location = models.CharField(max_length=50, verbose_name='当前库位', default='N/A')
  25. target_location = models.CharField(max_length=50, verbose_name='目标库位', default='N/A')
  26. available= models.BooleanField(default=True, verbose_name='可用')
  27. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='托盘状态')
  28. last_operation = models.DateTimeField(auto_now=True, verbose_name='最后操作时间')
  29. class Meta:
  30. db_table = 'container_list'
  31. verbose_name = 'ContainerList'
  32. verbose_name_plural = "ContainerList"
  33. ordering = ['-container_code']
  34. class ContainerDetailModel(models.Model):
  35. BATCH_STATUS=(
  36. (0, '空盘'),
  37. (1, '组盘'),
  38. (2, '在库'),
  39. (3, '已出库')
  40. )
  41. BATCH_CLASS = (
  42. (1, '成品'),
  43. (2, '空盘'),
  44. (3, '散盘'),
  45. )
  46. month = models.IntegerField(verbose_name='月份')
  47. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='details')
  48. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',blank=True, null=True)
  49. goods_class = models.IntegerField(verbose_name='货品类别',choices=BATCH_CLASS, default=1)
  50. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  51. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  52. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name='数量')
  53. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='出库数量')
  54. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  55. status = models.IntegerField(choices=BATCH_STATUS,default=0, verbose_name='状态')
  56. creater = models.CharField(max_length=50, verbose_name='创建人')
  57. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  58. update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
  59. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  60. class Meta:
  61. db_table = 'container_detail'
  62. verbose_name = 'ContainerDetail'
  63. verbose_name_plural = "ContainerDetail"
  64. ordering = ['-id']
  65. indexes = [
  66. models.Index(fields=['container']),
  67. models.Index(fields=['batch']),
  68. models.Index(fields=['goods_code']),
  69. models.Index(fields=['status']),
  70. ]
  71. def __str__(self):
  72. return f"{self.container.container_code} - {self.batch.bound_number if self.batch else 'N/A'} - {self.goods_code}"
  73. def save(self, *args, **kwargs):
  74. """
  75. 更新托盘上的物料数量,更新批次上的
  76. goods_in_qty(组盘数目)
  77. ,goods_in_location_qty(在库数目)
  78. ,goods_out_qty(出库数目)
  79. """
  80. # 在保存前调用信号处理器
  81. if self.pk:
  82. # 获取状态变化前的状态
  83. original = ContainerDetailModel.objects.get(pk=self.pk)
  84. original_status = original.status
  85. super().save(*args, **kwargs)
  86. # 更新批次数据
  87. if self.batch:
  88. # 避免循环更新 - 仅在数量相关字段变更时才更新
  89. if 'update_fields' not in kwargs or any(field in kwargs['update_fields'] for field in ['goods_qty', 'goods_out_qty','is_delete']):
  90. self.update_batch_stats()
  91. # # 根据出库数量更新状态
  92. # if self.goods_out_qty > self.goods_qty:
  93. # # 出库数量不能大于总数量
  94. # self.goods_out_qty = self.goods_qty
  95. # self.save(update_fields=['goods_out_qty'])
  96. # 根据当前数量状态更新状态
  97. if self.goods_out_qty >= self.goods_qty:
  98. self.status = 3 # 已出库
  99. super().save(*args, **kwargs)
  100. elif self.goods_qty - self.goods_out_qty > 0 and self.goods_out_qty > 0:
  101. self.status = 2 # 在库
  102. super().save(*args, **kwargs)
  103. if self.status == 3 and self.goods_qty - self.goods_out_qty > 0 :
  104. self.status = 2 # 在库
  105. super().save(*args, **kwargs)
  106. # 更新货物分类(关键修改点)
  107. if not kwargs.get('skip_class_update', False):
  108. # 获取托盘上所有物料(排除已删除和已出库的)
  109. details = ContainerDetailModel.objects.filter(
  110. container=self.container.id,
  111. is_delete=False
  112. ).exclude(status=3)
  113. # 统计不同批次数目
  114. batch_count = details.values('batch').distinct().count()
  115. new_class = 1 if batch_count == 1 else 3
  116. # 批量更新所有相关物料的goods_class
  117. details.exclude(goods_class=new_class).update(goods_class=new_class)
  118. # 如果当前物料的class需要更新
  119. if self.goods_class != new_class:
  120. self.goods_class = new_class
  121. super().save(update_fields=['goods_class'])
  122. def update_batch_stats(self):
  123. """更新批次的统计数据"""
  124. if not self.batch:
  125. return
  126. # 聚合托盘明细数据
  127. stats = ContainerDetailModel.objects.filter(
  128. batch=self.batch,
  129. is_delete=False
  130. ).aggregate(
  131. total_qty=models.Sum('goods_qty'),
  132. total_out_qty=models.Sum('goods_out_qty')
  133. )
  134. # 更新批次数据
  135. self.batch.goods_in_qty = stats['total_qty'] or 0
  136. self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0)
  137. self.batch.goods_out_qty = stats['total_out_qty'] or 0
  138. self.batch.save()
  139. def get_container_code(self):
  140. return self.container.container_code
  141. class ContainerDetailLogModel(models.Model):
  142. """托盘明细变更日志模型"""
  143. LOG_TYPES = (
  144. ('create', '创建'),
  145. ('update', '更新'),
  146. ('delete', '删除'),
  147. ('out', '出库'),
  148. ('cancel_out', '取消出库'),
  149. ('status_change', '状态变更'),
  150. )
  151. # 关联的托盘明细
  152. container_detail = models.ForeignKey(
  153. ContainerDetailModel,
  154. on_delete=models.CASCADE,
  155. related_name='logs'
  156. )
  157. # 日志类型
  158. log_type = models.CharField(
  159. max_length=20,
  160. choices=LOG_TYPES,
  161. verbose_name='日志类型'
  162. )
  163. # 原值
  164. old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
  165. old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True)
  166. old_status = models.IntegerField(
  167. choices=ContainerDetailModel.BATCH_STATUS,
  168. null=True,
  169. blank=True,
  170. verbose_name='原状态'
  171. )
  172. # 新值
  173. new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
  174. new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True)
  175. new_status = models.IntegerField(
  176. choices=ContainerDetailModel.BATCH_STATUS,
  177. null=True,
  178. blank=True,
  179. verbose_name='新状态'
  180. )
  181. # 元信息
  182. creater = models.CharField(max_length=50, verbose_name='操作人')
  183. create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  184. tobatchlog = models.BooleanField(default=False, verbose_name='是否转移到批次日志')
  185. class Meta:
  186. db_table = 'container_detail_log'
  187. verbose_name = '托盘明细变更日志'
  188. verbose_name_plural = "托盘明细变更日志"
  189. ordering = ['-create_time']
  190. def __str__(self):
  191. return f"{self.container_detail} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  192. class batchLogModel(models.Model):
  193. """批次日志模型"""
  194. LOG_TYPES = (
  195. ('create', '创建'),
  196. ('update', '更新'),
  197. ('delete', '删除'),
  198. ('out', '出库'),
  199. ('cancel_out', '取消出库'),
  200. ('status_change', '状态变更'),
  201. )
  202. bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='logs', null=True, blank=True)
  203. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, related_name='logs')
  204. log_type = models.CharField(max_length=20, choices=LOG_TYPES, verbose_name='日志类型')
  205. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  206. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  207. goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
  208. goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
  209. goods_in_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新入数量', null=True, blank=True)
  210. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出数量', null=True, blank=True)
  211. detail_logs = models.ManyToManyField(
  212. ContainerDetailLogModel,
  213. related_name='batch_logs',
  214. verbose_name='关联托盘日志'
  215. )
  216. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  217. class Meta:
  218. db_table = 'batch_log_from_container_log'
  219. verbose_name = '批次日志'
  220. verbose_name_plural = "批次日志"
  221. ordering = ['-create_time']
  222. def __str__(self):
  223. return f"{self.batch} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  224. def update_from_details(self):
  225. """从关联的托盘日志更新批次日志数据"""
  226. # 获取所有关联的托盘日志
  227. detail_logs = self.detail_logs.all()
  228. # 计算聚合值
  229. total_old_qty = detail_logs.aggregate(total=Sum('old_goods_qty'))['total'] or 0
  230. total_new_qty = detail_logs.aggregate(total=Sum('new_goods_qty'))['total'] or 0
  231. total_old_out = detail_logs.aggregate(total=Sum('old_goods_out_qty'))['total'] or 0
  232. total_new_out = detail_logs.aggregate(total=Sum('new_goods_out_qty'))['total'] or 0
  233. # 更新批次日志
  234. self.goods_in_qty = total_new_qty - total_old_qty
  235. self.goods_out_qty = total_new_out - total_old_out
  236. self.save()
  237. # 同时更新批次统计数据
  238. self.update_batch_stats()
  239. def update_batch_stats(self):
  240. """更新批次的统计数据"""
  241. # 聚合托盘明细数据
  242. stats = ContainerDetailModel.objects.filter(
  243. batch=self.batch,
  244. is_delete=False
  245. ).aggregate(
  246. total_qty=Sum('goods_qty'),
  247. total_out_qty=Sum('goods_out_qty')
  248. )
  249. # 更新批次数据
  250. self.batch.goods_in_qty = stats['total_qty'] or 0
  251. self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0)
  252. self.batch.goods_out_qty = stats['total_out_qty'] or 0
  253. self.batch.save()
  254. # 批次日志聚合处理器
  255. def aggregate_to_batch_log(container_log):
  256. """将托盘日志聚合到批次日志"""
  257. logger.info(f"开始聚合托盘日志: {container_log.id}")
  258. # 检查日志是否已处理
  259. if container_log.tobatchlog:
  260. logger.info(f"托盘日志 {container_log.id} 已处理过,跳过")
  261. return
  262. try:
  263. # 确定批次
  264. detail = container_log.container_detail
  265. if not detail:
  266. logger.warning(f"托盘日志 {container_log.id} 缺少关联的托盘明细")
  267. return
  268. batch = detail.batch
  269. if not batch:
  270. logger.warning(f"托盘明细 {detail.id} 缺少关联的批次")
  271. return
  272. # 确定操作类型
  273. log_type = container_log.log_type
  274. # 创建或获取批次日志(按类型和时间窗口分组)
  275. # 设置时间窗口(5分钟)
  276. time_window_start = container_log.create_time - timedelta(minutes=2)
  277. time_window_end = container_log.create_time + timedelta(minutes=2)
  278. # 查找相同批次、相同类型、在时间窗口内的批次日志
  279. existing_logs = batchLogModel.objects.filter(
  280. batch=batch,
  281. log_type=log_type,
  282. create_time__gte=time_window_start,
  283. create_time__lte=time_window_end
  284. ).order_by('create_time')
  285. if existing_logs.exists():
  286. # 使用时间窗口内最早的批次日志
  287. batch_log = existing_logs.first()
  288. logger.info(f"找到已有批次日志 {batch_log.id} 用于聚合")
  289. else:
  290. # 创建新的批次日志
  291. batch_log = batchLogModel.objects.create(
  292. batch=batch,
  293. log_type=log_type,
  294. goods_code=batch.goods_code,
  295. goods_desc=batch.goods_desc,
  296. goods_std=batch.goods_std,
  297. goods_unit=batch.goods_unit,
  298. create_time=container_log.create_time
  299. )
  300. logger.info(f"创建新批次日志 {batch_log.id}")
  301. # create_material_history(batch_log, update=True)
  302. # 将托盘日志关联到批次日志
  303. batch_log.detail_logs.add(container_log)
  304. # 从关联日志更新批次日志数据
  305. batch_log.update_from_details()
  306. # 根据日志类型添加关联单据信息
  307. if log_type == 'out' and not batch_log.bound:
  308. from bound.models import OutBoundDetailModel
  309. bound = OutBoundDetailModel.objects.filter(
  310. bound_batch_number=batch
  311. ).order_by('-create_time').first()
  312. if bound:
  313. batch_log.bound = bound.bound_list
  314. batch_log.save()
  315. logger.info(f"为批次日志 {batch_log.id} 添加出库单关联")
  316. if log_type == 'create' and not batch_log.bound:
  317. from bound.models import BoundDetailModel
  318. bound = BoundDetailModel.objects.filter(
  319. bound_batch=batch
  320. ).order_by('-create_time').first()
  321. if bound:
  322. batch_log.bound = bound.bound_list
  323. logger.info(f"为批次日志 {batch_log.id} 添加入库单关联")
  324. batch_log.save()
  325. # 标记日志已处理
  326. container_log.tobatchlog = True
  327. container_log.save()
  328. logger.info(f"成功聚合托盘日志 {container_log.id} 到批次日志 {batch_log.id}")
  329. return batch_log
  330. except Exception as e:
  331. logger.error(f"聚合托盘日志时出错: {e}", exc_info=True)
  332. raise
  333. # # 批次日志的信号处理器
  334. @receiver(post_save, sender=batchLogModel)
  335. def update_material_history(sender, instance, created, **kwargs):
  336. """批次日志保存后更新物料统计"""
  337. if created:
  338. # 创建物料变动历史记录
  339. create_material_history(instance, update=True)
  340. else:
  341. # 更新物料变动历史记录
  342. MaterialChangeHistory_obj =MaterialChangeHistory.objects.get(batch_log=instance)
  343. MaterialChangeHistory_obj.in_quantity = instance.goods_in_qty or Decimal('0')
  344. MaterialChangeHistory_obj.out_quantity = instance.goods_out_qty or Decimal('0')
  345. MaterialChangeHistory_obj.change_type = instance.log_type
  346. MaterialChangeHistory_obj.closing_quantity = MaterialChangeHistory_obj.opening_quantity + MaterialChangeHistory_obj.in_quantity - MaterialChangeHistory_obj.out_quantity
  347. MaterialChangeHistory_obj.save()
  348. def create_material_history( instance, update):
  349. """为每条批次日志创建物料变动历史记录"""
  350. from bound.models import MaterialStatistics
  351. if update:
  352. # 获取或创建物料统计
  353. stats, _ = MaterialStatistics.objects.get_or_create(
  354. goods_code=instance.goods_code,
  355. defaults={
  356. 'goods_desc': instance.goods_desc,
  357. 'goods_std': instance.goods_std or '待填写',
  358. 'goods_unit': instance.goods_unit or '待填写',
  359. }
  360. )
  361. # 计算期初数量(变动前的库存)
  362. opening_quantity = stats.total_quantity
  363. # 计算期末数量
  364. closing_quantity = opening_quantity
  365. if instance.goods_in_qty:
  366. closing_quantity += instance.goods_in_qty
  367. if instance.goods_out_qty:
  368. closing_quantity -= instance.goods_out_qty
  369. MaterialChangeHistory.objects.create(
  370. material_stats=stats,
  371. batch_log=instance,
  372. goods_code=instance.goods_code,
  373. goods_desc=instance.goods_desc,
  374. goods_std=instance.goods_std,
  375. goods_unit=instance.goods_unit,
  376. change_time=instance.create_time,
  377. in_quantity=instance.goods_in_qty or Decimal('0'),
  378. out_quantity=instance.goods_out_qty or Decimal('0'),
  379. change_type=instance.log_type,
  380. opening_quantity=opening_quantity,
  381. closing_quantity=closing_quantity
  382. )
  383. # 更新物料统计的实时库存
  384. stats.total_quantity = closing_quantity
  385. stats.save()
  386. # 简化的信号处理器
  387. @receiver(post_save, sender=ContainerDetailLogModel)
  388. def handle_container_detail_log(sender, instance, created, **kwargs):
  389. """创建托盘日志后立即关联到批次日志"""
  390. if created:
  391. try:
  392. aggregate_to_batch_log(instance)
  393. except Exception as e:
  394. print(f"Error aggregating log: {e}")
  395. @receiver(pre_save, sender=ContainerDetailModel)
  396. def container_detail_pre_save(sender, instance, **kwargs):
  397. """在托盘明细保存前记录变更"""
  398. if instance.pk:
  399. try:
  400. old_instance = ContainerDetailModel.objects.get(pk=instance.pk)
  401. logs = []
  402. # 数量变化日志
  403. if old_instance.goods_qty != instance.goods_qty:
  404. logs.append(ContainerDetailLogModel(
  405. container_detail=instance,
  406. log_type='update',
  407. old_goods_qty=old_instance.goods_qty,
  408. new_goods_qty=instance.goods_qty,
  409. creater=instance.creater
  410. ))
  411. # 出库数量变化日志
  412. if old_instance.goods_out_qty != instance.goods_out_qty:
  413. if old_instance.goods_out_qty < instance.goods_out_qty:
  414. log_type = 'out'
  415. else:
  416. log_type = 'cancel_out'
  417. logs.append(ContainerDetailLogModel(
  418. container_detail=instance,
  419. log_type=log_type,
  420. old_goods_out_qty=old_instance.goods_out_qty,
  421. new_goods_out_qty=instance.goods_out_qty,
  422. creater=instance.creater
  423. ))
  424. # 删除日志
  425. if old_instance.is_delete != instance.is_delete and instance.is_delete:
  426. logs.append(ContainerDetailLogModel(
  427. container_detail=instance,
  428. log_type='delete',
  429. old_goods_qty=old_instance.goods_qty,
  430. old_goods_out_qty=old_instance.goods_out_qty,
  431. new_goods_qty = old_instance.goods_qty,
  432. new_goods_out_qty = old_instance.goods_qty,
  433. old_status=old_instance.status,
  434. creater=instance.creater
  435. ))
  436. # 批量创建日志
  437. if logs:
  438. created_logs = ContainerDetailLogModel.objects.bulk_create(logs)
  439. for log in created_logs:
  440. # 由于bulk_create不会触发信号,我们手动调用信号处理函数
  441. handle_container_detail_log(ContainerDetailLogModel, log, created=True)
  442. except ContainerDetailModel.DoesNotExist:
  443. pass
  444. @receiver(post_save, sender=ContainerDetailModel)
  445. def container_detail_post_save(sender, instance, created, **kwargs):
  446. """在托盘明细新建时创建日志"""
  447. if created:
  448. ContainerDetailLogModel.objects.create(
  449. container_detail=instance,
  450. log_type='create',
  451. creater=instance.creater,
  452. new_goods_qty=instance.goods_qty,
  453. new_status=instance.status
  454. )
  455. class MaterialChangeHistory(models.Model):
  456. """物料库存变动历史记录(与批次日志多对一)"""
  457. material_stats = models.ForeignKey(
  458. MaterialStatistics,
  459. on_delete=models.CASCADE,
  460. related_name='history_records',
  461. verbose_name="关联物料"
  462. )
  463. # 与批次日志建立多对一关系
  464. batch_log = models.ForeignKey(batchLogModel,on_delete=models.CASCADE, related_name='material_history', verbose_name="关联批次日志",primary_key=False)
  465. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  466. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  467. goods_std = models.CharField(max_length=50, verbose_name='货品规格', null=True, blank=True)
  468. goods_unit = models.CharField(max_length=50, verbose_name='货品单位', null=True, blank=True)
  469. # 时间戳记录(使用批次日志的时间)
  470. change_time = models.DateTimeField(
  471. verbose_name="变动时间"
  472. )
  473. # 库存变动情况
  474. in_quantity = models.DecimalField(
  475. max_digits=10, decimal_places=3,
  476. default=Decimal('0'),
  477. verbose_name="入库数量"
  478. )
  479. out_quantity = models.DecimalField(
  480. max_digits=10, decimal_places=3,
  481. default=Decimal('0'),
  482. verbose_name="出库数量"
  483. )
  484. # 变更类型(从批次日志获取)
  485. change_type = models.CharField(
  486. max_length=20,
  487. verbose_name="变动类型"
  488. )
  489. # 变更时的库存快照
  490. opening_quantity = models.DecimalField(
  491. max_digits=10, decimal_places=3,
  492. default=Decimal('0'),
  493. verbose_name="期初数量"
  494. )
  495. closing_quantity = models.DecimalField(
  496. max_digits=10, decimal_places=3,
  497. default=Decimal('0'),
  498. verbose_name="期末数量"
  499. )
  500. class Meta:
  501. db_table = 'material_change_history'
  502. verbose_name = '物料变动历史'
  503. verbose_name_plural = "物料变动历史"
  504. ordering = ['-change_time']
  505. indexes = [
  506. models.Index(fields=['material_stats', 'change_time']),
  507. models.Index(fields=['change_time']),
  508. ]
  509. # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录
  510. class ContainerOperationModel(models.Model):
  511. OPERATION_TYPES = (
  512. ('container','组盘'),
  513. ('inbound', '入库'),
  514. ('outbound', '出库'),
  515. ('adjust', '调整'),
  516. )
  517. month = models.IntegerField(verbose_name='月份')
  518. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations')
  519. operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型')
  520. bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True)
  521. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',null=True, blank=True)
  522. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  523. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  524. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  525. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  526. operator = models.CharField(max_length=50, verbose_name='操作人')
  527. timestamp = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  528. from_location = models.CharField(max_length=50, null=True, verbose_name='原库位')
  529. to_location = models.CharField(max_length=50, null=True, verbose_name='目标库位')
  530. memo = models.TextField(null=True, verbose_name='备注')
  531. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  532. class Meta:
  533. db_table = 'container_operation'
  534. verbose_name = 'ContainerOperation'
  535. verbose_name_plural = "ContainerOperation"
  536. ordering = ['-timestamp']
  537. class ContainerWCSModel(models.Model):
  538. TASK_STATUS = (
  539. (100, '等待中'),
  540. (101, '处理中'),
  541. (102, '已暂停'),
  542. (103, '入库中'),
  543. (200, '已发送'),
  544. (300, '已完成'),
  545. (400, '失败'),
  546. )
  547. taskid = models.CharField(max_length=50, verbose_name='任务ID')
  548. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次',null=True, blank=True )
  549. batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True )
  550. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单',null=True, blank=True )
  551. batch_number = models.CharField(max_length=50, verbose_name='批次号',null=True, blank=True )
  552. sequence = models.BigIntegerField(verbose_name='任务顺序')
  553. priority = models.IntegerField(default=100, verbose_name='优先级')
  554. month = models.IntegerField(verbose_name='月份')
  555. tasktype = models.CharField(max_length=50, verbose_name='任务类型')
  556. tasknumber = models.IntegerField(verbose_name='任务号',unique=True)
  557. order_number = models.IntegerField(verbose_name='c_number')
  558. container = models.CharField(max_length=50, verbose_name='托盘号')
  559. current_location = models.CharField(max_length=50, verbose_name='当前库位')
  560. target_location = models.CharField(max_length=50, verbose_name='目标库位')
  561. message = models.TextField(verbose_name='消息')
  562. working = models.IntegerField(default = 1,verbose_name='工作状态')
  563. status = models.IntegerField(choices=TASK_STATUS, default=100, verbose_name='状态')
  564. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  565. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  566. class Meta:
  567. db_table = 'container_wcs'
  568. verbose_name = 'ContainerWCS'
  569. verbose_name_plural = "ContainerWCS"
  570. ordering = ['-create_time']
  571. def to_dict(self):
  572. return {
  573. 'container': self.container,
  574. 'current_location': self.current_location,
  575. 'month' : self.month,
  576. 'target_location': self.target_location,
  577. 'tasktype': self.tasktype,
  578. 'taskid': self.taskid,
  579. 'taskNumber': self.tasknumber-20000000000,
  580. 'message': self.message,
  581. 'container': self.container,
  582. 'status': self.status
  583. }
  584. def __str__(self):
  585. return f"{self.taskid} - {self.get_status_display()}"
  586. # 这里的批次详情是主入库申请单下的子批次
  587. class TaskModel(models.Model):
  588. task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, related_name='tasks')
  589. batch_detail = models.ForeignKey(BoundDetailModel, on_delete=models.CASCADE, verbose_name='批次详情')
  590. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  591. class Meta:
  592. db_table = 'task'
  593. verbose_name = 'Task'
  594. verbose_name_plural = "Task"
  595. ordering = ['-id']
  596. class out_batch_detail(models.Model):
  597. out_bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  598. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  599. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  600. out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  601. last_out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='上次出库数量')
  602. working = models.IntegerField(default = 1,verbose_name='工作状态')
  603. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  604. class Meta:
  605. db_table = 'out_batch_detail'
  606. verbose_name = 'OutBatchDetail'
  607. verbose_name_plural = "OutBatchDetail"
  608. ordering = ['container']