models.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. from django.db import models
  2. from bound.models import BoundBatchModel,BoundDetailModel,OutBatchModel,BoundListModel,BoundListModel
  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. # Create your models here.
  8. # 主表:托盘数据
  9. class ContainerListModel(models.Model):
  10. CONTAINER_STATUS = (
  11. (0, '空置'),
  12. (1, '入库中'),
  13. (2, '在库'),
  14. (3, '出库中'),
  15. (4, '已出库'),
  16. (5, '空托盘组')
  17. )
  18. container_code = models.IntegerField( verbose_name='托盘编号',unique=True)
  19. current_location = models.CharField(max_length=50, verbose_name='当前库位', default='N/A')
  20. target_location = models.CharField(max_length=50, verbose_name='目标库位', default='N/A')
  21. available= models.BooleanField(default=True, verbose_name='可用')
  22. status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='托盘状态')
  23. last_operation = models.DateTimeField(auto_now=True, verbose_name='最后操作时间')
  24. class Meta:
  25. db_table = 'container_list'
  26. verbose_name = 'ContainerList'
  27. verbose_name_plural = "ContainerList"
  28. ordering = ['-container_code']
  29. class ContainerDetailModel(models.Model):
  30. BATCH_STATUS=(
  31. (0, '空盘'),
  32. (1, '组盘'),
  33. (2, '在库'),
  34. (3, '已出库')
  35. )
  36. BATCH_CLASS = (
  37. (1, '成品'),
  38. (2, '空盘'),
  39. (3, '散盘'),
  40. )
  41. month = models.IntegerField(verbose_name='月份')
  42. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='details')
  43. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',blank=True, null=True)
  44. goods_class = models.IntegerField(verbose_name='货品类别',choices=BATCH_CLASS, default=1)
  45. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  46. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  47. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name='数量')
  48. goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='出库数量')
  49. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  50. status = models.IntegerField(choices=BATCH_STATUS,default=0, verbose_name='状态')
  51. creater = models.CharField(max_length=50, verbose_name='创建人')
  52. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  53. update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
  54. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  55. class Meta:
  56. db_table = 'container_detail'
  57. verbose_name = 'ContainerDetail'
  58. verbose_name_plural = "ContainerDetail"
  59. ordering = ['-id']
  60. indexes = [
  61. models.Index(fields=['container']),
  62. models.Index(fields=['batch']),
  63. models.Index(fields=['goods_code']),
  64. models.Index(fields=['status']),
  65. ]
  66. def __str__(self):
  67. return f"{self.container.container_code} - {self.batch.bound_number if self.batch else 'N/A'} - {self.goods_code}"
  68. def save(self, *args, **kwargs):
  69. """
  70. 更新托盘上的物料数量,更新批次上的
  71. goods_in_qty(组盘数目)
  72. ,goods_in_location_qty(在库数目)
  73. ,goods_out_qty(出库数目)
  74. """
  75. # 在保存前调用信号处理器
  76. if self.pk:
  77. # 获取状态变化前的状态
  78. original = ContainerDetailModel.objects.get(pk=self.pk)
  79. original_status = original.status
  80. super().save(*args, **kwargs)
  81. # 更新批次数据
  82. if self.batch:
  83. # 避免循环更新 - 仅在数量相关字段变更时才更新
  84. if 'update_fields' not in kwargs or any(field in kwargs['update_fields'] for field in ['goods_qty', 'goods_out_qty','is_delete']):
  85. self.update_batch_stats()
  86. # # 根据出库数量更新状态
  87. # if self.goods_out_qty > self.goods_qty:
  88. # # 出库数量不能大于总数量
  89. # self.goods_out_qty = self.goods_qty
  90. # self.save(update_fields=['goods_out_qty'])
  91. # 根据当前数量状态更新状态
  92. if self.goods_out_qty >= self.goods_qty:
  93. self.status = 3 # 已出库
  94. super().save(*args, **kwargs)
  95. elif self.goods_qty - self.goods_out_qty > 0 and self.goods_out_qty > 0:
  96. self.status = 2 # 在库
  97. super().save(*args, **kwargs)
  98. if self.status == 3 and self.goods_qty - self.goods_out_qty > 0 :
  99. self.status = 2 # 在库
  100. super().save(*args, **kwargs)
  101. # 更新货物分类(关键修改点)
  102. if not kwargs.get('skip_class_update', False):
  103. # 获取托盘上所有物料(排除已删除和已出库的)
  104. details = ContainerDetailModel.objects.filter(
  105. container=self.container.id,
  106. is_delete=False
  107. ).exclude(status=3)
  108. # 统计不同批次数目
  109. batch_count = details.values('batch').distinct().count()
  110. new_class = 1 if batch_count == 1 else 3
  111. # 批量更新所有相关物料的goods_class
  112. details.exclude(goods_class=new_class).update(goods_class=new_class)
  113. # 如果当前物料的class需要更新
  114. if self.goods_class != new_class:
  115. self.goods_class = new_class
  116. super().save(update_fields=['goods_class'])
  117. def update_batch_stats(self):
  118. """更新批次的统计数据"""
  119. if not self.batch:
  120. return
  121. # 聚合托盘明细数据
  122. stats = ContainerDetailModel.objects.filter(
  123. batch=self.batch,
  124. is_delete=False
  125. ).aggregate(
  126. total_qty=models.Sum('goods_qty'),
  127. total_out_qty=models.Sum('goods_out_qty')
  128. )
  129. # 更新批次数据
  130. self.batch.goods_in_qty = stats['total_qty'] or 0
  131. self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0)
  132. self.batch.goods_out_qty = stats['total_out_qty'] or 0
  133. self.batch.save()
  134. def get_container_code(self):
  135. return self.container.container_code
  136. class batchLogModel(models.Model):
  137. """批次日志模型"""
  138. LOG_TYPES = (
  139. ('create', '创建'),
  140. ('update', '更新'),
  141. ('delete', '删除'),
  142. ('out', '出库'),
  143. ('cancel_out', '取消出库'),
  144. ('status_change', '状态变更'),
  145. )
  146. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, related_name='logs')
  147. log_type = models.CharField(max_length=20, choices=LOG_TYPES, verbose_name='日志类型')
  148. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  149. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  150. old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
  151. new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
  152. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  153. class Meta:
  154. db_table = 'batch_log'
  155. verbose_name = '批次日志'
  156. verbose_name_plural = "批次日志"
  157. ordering = ['-create_time']
  158. def __str__(self):
  159. return f"{self.batch} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  160. class ContainerDetailLogModel(models.Model):
  161. """托盘明细变更日志模型"""
  162. LOG_TYPES = (
  163. ('create', '创建'),
  164. ('update', '更新'),
  165. ('delete', '删除'),
  166. ('out', '出库'),
  167. ('cancel_out', '取消出库'),
  168. ('status_change', '状态变更'),
  169. )
  170. # 关联的托盘明细
  171. container_detail = models.ForeignKey(
  172. ContainerDetailModel,
  173. on_delete=models.CASCADE,
  174. related_name='logs'
  175. )
  176. # 日志类型
  177. log_type = models.CharField(
  178. max_length=20,
  179. choices=LOG_TYPES,
  180. verbose_name='日志类型'
  181. )
  182. # 原值
  183. old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
  184. old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True)
  185. old_status = models.IntegerField(
  186. choices=ContainerDetailModel.BATCH_STATUS,
  187. null=True,
  188. blank=True,
  189. verbose_name='原状态'
  190. )
  191. # 新值
  192. new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
  193. new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True)
  194. new_status = models.IntegerField(
  195. choices=ContainerDetailModel.BATCH_STATUS,
  196. null=True,
  197. blank=True,
  198. verbose_name='新状态'
  199. )
  200. # 元信息
  201. creater = models.CharField(max_length=50, verbose_name='操作人')
  202. create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  203. tobatchlog = models.BooleanField(default=False, verbose_name='是否转移到批次日志')
  204. class Meta:
  205. db_table = 'container_detail_log'
  206. verbose_name = '托盘明细变更日志'
  207. verbose_name_plural = "托盘明细变更日志"
  208. ordering = ['-create_time']
  209. def __str__(self):
  210. return f"{self.container_detail} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  211. # # 根据时间、类型来判断返回当前的主单、批次,累计出入库数量,并将tobatchlog设置为True,将日志记录转移到批次日志中
  212. def find_batch_by_time_and_type(ContainerDetailLogModel_obj):
  213. time = ContainerDetailLogModel_obj.create_time
  214. operation_type= ContainerDetailLogModel_obj.log_type
  215. batch_number = ContainerDetailLogModel_obj.container_detail.batch.bound_number
  216. if operation_type == 'create':
  217. # 这里查找逻辑是先找到最近的批次日志,然后再找主单
  218. batchlogobj = batchLogModel.objects.filter(log_type='create', batch__bound_number=batch_number, create_time__lte=time).order_by('-create_time').first()
  219. if batchlogobj:
  220. @receiver(pre_save, sender=ContainerDetailModel)
  221. def container_detail_pre_save(sender, instance, **kwargs):
  222. """在托盘明细保存前记录变更"""
  223. # 检查是否是新建对象
  224. if instance.pk:
  225. try:
  226. # 获取数据库中的当前对象
  227. old_instance = ContainerDetailModel.objects.get(pk=instance.pk)
  228. except ContainerDetailModel.DoesNotExist:
  229. return
  230. # 创建日志记录
  231. log = ContainerDetailLogModel(container_detail=instance, creater=instance.creater)
  232. # 检查数量变化
  233. if old_instance.goods_qty != instance.goods_qty:
  234. log.old_goods_qty = old_instance.goods_qty
  235. log.new_goods_qty = instance.goods_qty
  236. log.log_type = 'update'
  237. log.save()
  238. # 检查出库数量变化
  239. if old_instance.goods_out_qty != instance.goods_out_qty:
  240. log.old_goods_out_qty = old_instance.goods_out_qty
  241. log.new_goods_out_qty = instance.goods_out_qty
  242. if log.old_goods_out_qty < log.new_goods_out_qty:
  243. log.log_type = 'out'
  244. else:
  245. log.log_type = 'cancel_out'
  246. log.save()
  247. if old_instance.is_delete != instance.is_delete:
  248. log.log_type = 'delete'
  249. log.old_goods_qty = old_instance.goods_qty
  250. log.old_goods_out_qty = old_instance.goods_out_qty
  251. log.old_status = old_instance.status
  252. log.new_goods_qty = instance.goods_qty
  253. log.new_goods_out_qty = instance.goods_out_qty
  254. log.new_status = instance.status
  255. log.save()
  256. # # 检查状态变化
  257. # if old_instance.status != instance.status:
  258. # log.old_status = old_instance.status
  259. # log.new_status = instance.status
  260. # log.log_type = 'status_change'
  261. # # 保存日志记录
  262. # if hasattr(log, 'log_type'):
  263. # log.save()
  264. @receiver(post_save, sender=ContainerDetailModel)
  265. def container_detail_post_save(sender, instance, created, **kwargs):
  266. """在托盘明细保存后记录创建"""
  267. if created:
  268. # 为新建的对象创建日志
  269. ContainerDetailLogModel.objects.create(
  270. container_detail=instance,
  271. log_type='create',
  272. creater=instance.creater,
  273. new_goods_qty=instance.goods_qty,
  274. new_status=instance.status
  275. )
  276. # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录
  277. class ContainerOperationModel(models.Model):
  278. OPERATION_TYPES = (
  279. ('container','组盘'),
  280. ('inbound', '入库'),
  281. ('outbound', '出库'),
  282. ('adjust', '调整'),
  283. )
  284. month = models.IntegerField(verbose_name='月份')
  285. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations')
  286. operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型')
  287. bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True)
  288. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',null=True, blank=True)
  289. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  290. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  291. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  292. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  293. operator = models.CharField(max_length=50, verbose_name='操作人')
  294. timestamp = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  295. from_location = models.CharField(max_length=50, null=True, verbose_name='原库位')
  296. to_location = models.CharField(max_length=50, null=True, verbose_name='目标库位')
  297. memo = models.TextField(null=True, verbose_name='备注')
  298. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  299. class Meta:
  300. db_table = 'container_operation'
  301. verbose_name = 'ContainerOperation'
  302. verbose_name_plural = "ContainerOperation"
  303. ordering = ['-timestamp']
  304. class ContainerWCSModel(models.Model):
  305. TASK_STATUS = (
  306. (100, '等待中'),
  307. (101, '处理中'),
  308. (102, '已暂停'),
  309. (103, '入库中'),
  310. (200, '已发送'),
  311. (300, '已完成'),
  312. (400, '失败'),
  313. )
  314. taskid = models.CharField(max_length=50, verbose_name='任务ID')
  315. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次',null=True, blank=True )
  316. batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True )
  317. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单',null=True, blank=True )
  318. batch_number = models.CharField(max_length=50, verbose_name='批次号',null=True, blank=True )
  319. sequence = models.BigIntegerField(verbose_name='任务顺序')
  320. priority = models.IntegerField(default=100, verbose_name='优先级')
  321. month = models.IntegerField(verbose_name='月份')
  322. tasktype = models.CharField(max_length=50, verbose_name='任务类型')
  323. tasknumber = models.IntegerField(verbose_name='任务号',unique=True)
  324. order_number = models.IntegerField(verbose_name='c_number')
  325. container = models.CharField(max_length=50, verbose_name='托盘号')
  326. current_location = models.CharField(max_length=50, verbose_name='当前库位')
  327. target_location = models.CharField(max_length=50, verbose_name='目标库位')
  328. message = models.TextField(verbose_name='消息')
  329. working = models.IntegerField(default = 1,verbose_name='工作状态')
  330. status = models.IntegerField(choices=TASK_STATUS, default=100, verbose_name='状态')
  331. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  332. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  333. class Meta:
  334. db_table = 'container_wcs'
  335. verbose_name = 'ContainerWCS'
  336. verbose_name_plural = "ContainerWCS"
  337. ordering = ['-create_time']
  338. def to_dict(self):
  339. return {
  340. 'container': self.container,
  341. 'current_location': self.current_location,
  342. 'month' : self.month,
  343. 'target_location': self.target_location,
  344. 'tasktype': self.tasktype,
  345. 'taskid': self.taskid,
  346. 'taskNumber': self.tasknumber-20000000000,
  347. 'message': self.message,
  348. 'container': self.container,
  349. 'status': self.status
  350. }
  351. def __str__(self):
  352. return f"{self.taskid} - {self.get_status_display()}"
  353. # 这里的批次详情是主入库申请单下的子批次
  354. class TaskModel(models.Model):
  355. task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, related_name='tasks')
  356. batch_detail = models.ForeignKey(BoundDetailModel, on_delete=models.CASCADE, verbose_name='批次详情')
  357. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  358. class Meta:
  359. db_table = 'task'
  360. verbose_name = 'Task'
  361. verbose_name_plural = "Task"
  362. ordering = ['-id']
  363. class out_batch_detail(models.Model):
  364. out_bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  365. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  366. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  367. out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  368. last_out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='上次出库数量')
  369. working = models.IntegerField(default = 1,verbose_name='工作状态')
  370. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  371. class Meta:
  372. db_table = 'out_batch_detail'
  373. verbose_name = 'OutBatchDetail'
  374. verbose_name_plural = "OutBatchDetail"
  375. ordering = ['container']