models.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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 ContainerDetailLogModel(models.Model):
  137. """托盘明细变更日志模型"""
  138. LOG_TYPES = (
  139. ('create', '创建'),
  140. ('update', '更新'),
  141. ('delete', '删除'),
  142. ('out', '出库'),
  143. ('cancel_out', '取消出库'),
  144. ('status_change', '状态变更'),
  145. )
  146. # 关联的托盘明细
  147. container_detail = models.ForeignKey(
  148. ContainerDetailModel,
  149. on_delete=models.CASCADE,
  150. related_name='logs'
  151. )
  152. # 日志类型
  153. log_type = models.CharField(
  154. max_length=20,
  155. choices=LOG_TYPES,
  156. verbose_name='日志类型'
  157. )
  158. # 原值
  159. old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True)
  160. old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True)
  161. old_status = models.IntegerField(
  162. choices=ContainerDetailModel.BATCH_STATUS,
  163. null=True,
  164. blank=True,
  165. verbose_name='原状态'
  166. )
  167. # 新值
  168. new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True)
  169. new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True)
  170. new_status = models.IntegerField(
  171. choices=ContainerDetailModel.BATCH_STATUS,
  172. null=True,
  173. blank=True,
  174. verbose_name='新状态'
  175. )
  176. # 元信息
  177. creater = models.CharField(max_length=50, verbose_name='操作人')
  178. create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  179. class Meta:
  180. db_table = 'container_detail_log'
  181. verbose_name = '托盘明细变更日志'
  182. verbose_name_plural = "托盘明细变更日志"
  183. ordering = ['-create_time']
  184. def __str__(self):
  185. return f"{self.container_detail} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}"
  186. @receiver(pre_save, sender=ContainerDetailModel)
  187. def container_detail_pre_save(sender, instance, **kwargs):
  188. """在托盘明细保存前记录变更"""
  189. # 检查是否是新建对象
  190. if instance.pk:
  191. try:
  192. # 获取数据库中的当前对象
  193. old_instance = ContainerDetailModel.objects.get(pk=instance.pk)
  194. except ContainerDetailModel.DoesNotExist:
  195. return
  196. # 创建日志记录
  197. log = ContainerDetailLogModel(container_detail=instance, creater=instance.creater)
  198. # 检查数量变化
  199. if old_instance.goods_qty != instance.goods_qty:
  200. log.old_goods_qty = old_instance.goods_qty
  201. log.new_goods_qty = instance.goods_qty
  202. log.log_type = 'update'
  203. log.save()
  204. # 检查出库数量变化
  205. if old_instance.goods_out_qty != instance.goods_out_qty:
  206. log.old_goods_out_qty = old_instance.goods_out_qty
  207. log.new_goods_out_qty = instance.goods_out_qty
  208. if log.old_goods_out_qty < log.new_goods_out_qty:
  209. log.log_type = 'out'
  210. else:
  211. log.log_type = 'cancel_out'
  212. log.save()
  213. # # 检查状态变化
  214. # if old_instance.status != instance.status:
  215. # log.old_status = old_instance.status
  216. # log.new_status = instance.status
  217. # log.log_type = 'status_change'
  218. # # 保存日志记录
  219. # if hasattr(log, 'log_type'):
  220. # log.save()
  221. @receiver(post_save, sender=ContainerDetailModel)
  222. def container_detail_post_save(sender, instance, created, **kwargs):
  223. """在托盘明细保存后记录创建"""
  224. if created:
  225. # 为新建的对象创建日志
  226. ContainerDetailLogModel.objects.create(
  227. container_detail=instance,
  228. log_type='create',
  229. creater=instance.creater,
  230. new_goods_qty=instance.goods_qty,
  231. new_status=instance.status
  232. )
  233. # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录
  234. class ContainerOperationModel(models.Model):
  235. OPERATION_TYPES = (
  236. ('container','组盘'),
  237. ('inbound', '入库'),
  238. ('outbound', '出库'),
  239. ('adjust', '调整'),
  240. )
  241. month = models.IntegerField(verbose_name='月份')
  242. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations')
  243. operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型')
  244. bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True)
  245. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',null=True, blank=True)
  246. goods_code = models.CharField(max_length=50, verbose_name='货品编码')
  247. goods_desc = models.CharField(max_length=100, verbose_name='货品描述')
  248. goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  249. goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量')
  250. operator = models.CharField(max_length=50, verbose_name='操作人')
  251. timestamp = models.DateTimeField(auto_now_add=True, verbose_name='操作时间')
  252. from_location = models.CharField(max_length=50, null=True, verbose_name='原库位')
  253. to_location = models.CharField(max_length=50, null=True, verbose_name='目标库位')
  254. memo = models.TextField(null=True, verbose_name='备注')
  255. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  256. class Meta:
  257. db_table = 'container_operation'
  258. verbose_name = 'ContainerOperation'
  259. verbose_name_plural = "ContainerOperation"
  260. ordering = ['-timestamp']
  261. class ContainerWCSModel(models.Model):
  262. TASK_STATUS = (
  263. (100, '等待中'),
  264. (101, '处理中'),
  265. (102, '已暂停'),
  266. (103, '入库中'),
  267. (200, '已发送'),
  268. (300, '已完成'),
  269. (400, '失败'),
  270. )
  271. taskid = models.CharField(max_length=50, verbose_name='任务ID')
  272. batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次',null=True, blank=True )
  273. batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True )
  274. bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单',null=True, blank=True )
  275. batch_number = models.CharField(max_length=50, verbose_name='批次号',null=True, blank=True )
  276. sequence = models.BigIntegerField(verbose_name='任务顺序')
  277. priority = models.IntegerField(default=100, verbose_name='优先级')
  278. month = models.IntegerField(verbose_name='月份')
  279. tasktype = models.CharField(max_length=50, verbose_name='任务类型')
  280. tasknumber = models.IntegerField(verbose_name='任务号',unique=True)
  281. order_number = models.IntegerField(verbose_name='c_number')
  282. container = models.CharField(max_length=50, verbose_name='托盘号')
  283. current_location = models.CharField(max_length=50, verbose_name='当前库位')
  284. target_location = models.CharField(max_length=50, verbose_name='目标库位')
  285. message = models.TextField(verbose_name='消息')
  286. working = models.IntegerField(default = 1,verbose_name='工作状态')
  287. status = models.IntegerField(choices=TASK_STATUS, default=100, verbose_name='状态')
  288. create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  289. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  290. class Meta:
  291. db_table = 'container_wcs'
  292. verbose_name = 'ContainerWCS'
  293. verbose_name_plural = "ContainerWCS"
  294. ordering = ['-create_time']
  295. def to_dict(self):
  296. return {
  297. 'container': self.container,
  298. 'current_location': self.current_location,
  299. 'month' : self.month,
  300. 'target_location': self.target_location,
  301. 'tasktype': self.tasktype,
  302. 'taskid': self.taskid,
  303. 'taskNumber': self.tasknumber-20000000000,
  304. 'message': self.message,
  305. 'container': self.container,
  306. 'status': self.status
  307. }
  308. def __str__(self):
  309. return f"{self.taskid} - {self.get_status_display()}"
  310. # 这里的批次详情是主入库申请单下的子批次
  311. class TaskModel(models.Model):
  312. task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, related_name='tasks')
  313. batch_detail = models.ForeignKey(BoundDetailModel, on_delete=models.CASCADE, verbose_name='批次详情')
  314. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  315. class Meta:
  316. db_table = 'task'
  317. verbose_name = 'Task'
  318. verbose_name_plural = "Task"
  319. ordering = ['-id']
  320. class out_batch_detail(models.Model):
  321. out_bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  322. container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='out_batch_details')
  323. container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细')
  324. out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量')
  325. last_out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='上次出库数量')
  326. working = models.IntegerField(default = 1,verbose_name='工作状态')
  327. is_delete = models.BooleanField(default=False, verbose_name='是否删除')
  328. class Meta:
  329. db_table = 'out_batch_detail'
  330. verbose_name = 'OutBatchDetail'
  331. verbose_name_plural = "OutBatchDetail"
  332. ordering = ['container']