from django.db import models from bound.models import BoundBatchModel,BoundDetailModel,OutBatchModel,BoundListModel,BoundListModel from django.utils import timezone from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from decimal import Decimal from django.db.models import Sum from datetime import timedelta from django.db.models import Q import logging logger = logging.getLogger(__name__) # Create your models here. # 主表:托盘数据 class ContainerListModel(models.Model): CONTAINER_STATUS = ( (0, '空置'), (1, '入库中'), (2, '在库'), (3, '出库中'), (4, '已出库'), (5, '空托盘组') ) container_code = models.IntegerField( verbose_name='托盘编号',unique=True) current_location = models.CharField(max_length=50, verbose_name='当前库位', default='N/A') target_location = models.CharField(max_length=50, verbose_name='目标库位', default='N/A') available= models.BooleanField(default=True, verbose_name='可用') status = models.IntegerField(choices=CONTAINER_STATUS, default=0, verbose_name='托盘状态') last_operation = models.DateTimeField(auto_now=True, verbose_name='最后操作时间') class Meta: db_table = 'container_list' verbose_name = 'ContainerList' verbose_name_plural = "ContainerList" ordering = ['-container_code'] class ContainerDetailModel(models.Model): BATCH_STATUS=( (0, '空盘'), (1, '组盘'), (2, '在库'), (3, '已出库') ) BATCH_CLASS = ( (1, '成品'), (2, '空盘'), (3, '散盘'), ) month = models.IntegerField(verbose_name='月份') container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='details') batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',blank=True, null=True) goods_class = models.IntegerField(verbose_name='货品类别',choices=BATCH_CLASS, default=1) goods_code = models.CharField(max_length=50, verbose_name='货品编码') goods_desc = models.CharField(max_length=100, verbose_name='货品描述') goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'), verbose_name='数量') goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='出库数量') goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量') status = models.IntegerField(choices=BATCH_STATUS,default=0, verbose_name='状态') creater = models.CharField(max_length=50, verbose_name='创建人') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') is_delete = models.BooleanField(default=False, verbose_name='是否删除') class Meta: db_table = 'container_detail' verbose_name = 'ContainerDetail' verbose_name_plural = "ContainerDetail" ordering = ['-id'] indexes = [ models.Index(fields=['container']), models.Index(fields=['batch']), models.Index(fields=['goods_code']), models.Index(fields=['status']), ] def __str__(self): return f"{self.container.container_code} - {self.batch.bound_number if self.batch else 'N/A'} - {self.goods_code}" def save(self, *args, **kwargs): """ 更新托盘上的物料数量,更新批次上的 goods_in_qty(组盘数目) ,goods_in_location_qty(在库数目) ,goods_out_qty(出库数目) """ # 在保存前调用信号处理器 if self.pk: # 获取状态变化前的状态 original = ContainerDetailModel.objects.get(pk=self.pk) original_status = original.status super().save(*args, **kwargs) # 更新批次数据 if self.batch: # 避免循环更新 - 仅在数量相关字段变更时才更新 if 'update_fields' not in kwargs or any(field in kwargs['update_fields'] for field in ['goods_qty', 'goods_out_qty','is_delete']): self.update_batch_stats() # # 根据出库数量更新状态 # if self.goods_out_qty > self.goods_qty: # # 出库数量不能大于总数量 # self.goods_out_qty = self.goods_qty # self.save(update_fields=['goods_out_qty']) # 根据当前数量状态更新状态 if self.goods_out_qty >= self.goods_qty: self.status = 3 # 已出库 super().save(*args, **kwargs) elif self.goods_qty - self.goods_out_qty > 0 and self.goods_out_qty > 0: self.status = 2 # 在库 super().save(*args, **kwargs) if self.status == 3 and self.goods_qty - self.goods_out_qty > 0 : self.status = 2 # 在库 super().save(*args, **kwargs) # 更新货物分类(关键修改点) if not kwargs.get('skip_class_update', False): # 获取托盘上所有物料(排除已删除和已出库的) details = ContainerDetailModel.objects.filter( container=self.container.id, is_delete=False ).exclude(status=3) # 统计不同批次数目 batch_count = details.values('batch').distinct().count() new_class = 1 if batch_count == 1 else 3 # 批量更新所有相关物料的goods_class details.exclude(goods_class=new_class).update(goods_class=new_class) # 如果当前物料的class需要更新 if self.goods_class != new_class: self.goods_class = new_class super().save(update_fields=['goods_class']) def update_batch_stats(self): """更新批次的统计数据""" if not self.batch: return # 聚合托盘明细数据 stats = ContainerDetailModel.objects.filter( batch=self.batch, is_delete=False ).aggregate( total_qty=models.Sum('goods_qty'), total_out_qty=models.Sum('goods_out_qty') ) # 更新批次数据 self.batch.goods_in_qty = stats['total_qty'] or 0 self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0) self.batch.goods_out_qty = stats['total_out_qty'] or 0 self.batch.save() def get_container_code(self): return self.container.container_code class ContainerDetailLogModel(models.Model): """托盘明细变更日志模型""" LOG_TYPES = ( ('create', '创建'), ('update', '更新'), ('delete', '删除'), ('out', '出库'), ('cancel_out', '取消出库'), ('status_change', '状态变更'), ) # 关联的托盘明细 container_detail = models.ForeignKey( ContainerDetailModel, on_delete=models.CASCADE, related_name='logs' ) # 日志类型 log_type = models.CharField( max_length=20, choices=LOG_TYPES, verbose_name='日志类型' ) # 原值 old_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原数量', null=True, blank=True) old_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='原出库数量', null=True, blank=True) old_status = models.IntegerField( choices=ContainerDetailModel.BATCH_STATUS, null=True, blank=True, verbose_name='原状态' ) # 新值 new_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新数量', null=True, blank=True) new_goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出库数量', null=True, blank=True) new_status = models.IntegerField( choices=ContainerDetailModel.BATCH_STATUS, null=True, blank=True, verbose_name='新状态' ) # 元信息 creater = models.CharField(max_length=50, verbose_name='操作人') create_time = models.DateTimeField(auto_now_add=True, verbose_name='操作时间') tobatchlog = models.BooleanField(default=False, verbose_name='是否转移到批次日志') class Meta: db_table = 'container_detail_log' verbose_name = '托盘明细变更日志' verbose_name_plural = "托盘明细变更日志" ordering = ['-create_time'] def __str__(self): return f"{self.container_detail} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}" class batchLogModel(models.Model): """批次日志模型""" LOG_TYPES = ( ('create', '创建'), ('update', '更新'), ('delete', '删除'), ('out', '出库'), ('cancel_out', '取消出库'), ('status_change', '状态变更'), ) bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='logs', null=True, blank=True) batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, related_name='logs') log_type = models.CharField(max_length=20, choices=LOG_TYPES, verbose_name='日志类型') goods_code = models.CharField(max_length=50, verbose_name='货品编码') goods_desc = models.CharField(max_length=100, verbose_name='货品描述') goods_in_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新入数量', null=True, blank=True) goods_out_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='新出数量', null=True, blank=True) detail_logs = models.ManyToManyField( ContainerDetailLogModel, related_name='batch_logs', verbose_name='关联托盘日志' ) create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: db_table = 'batch_log_from_container_log' verbose_name = '批次日志' verbose_name_plural = "批次日志" ordering = ['-create_time'] def __str__(self): return f"{self.batch} - {self.get_log_type_display()} - {self.create_time.strftime('%Y-%m-%d %H:%M:%S')}" def update_from_details(self): """从关联的托盘日志更新批次日志数据""" # 获取所有关联的托盘日志 detail_logs = self.detail_logs.all() # 计算聚合值 total_old_qty = detail_logs.aggregate(total=Sum('old_goods_qty'))['total'] or 0 total_new_qty = detail_logs.aggregate(total=Sum('new_goods_qty'))['total'] or 0 total_old_out = detail_logs.aggregate(total=Sum('old_goods_out_qty'))['total'] or 0 total_new_out = detail_logs.aggregate(total=Sum('new_goods_out_qty'))['total'] or 0 # 更新批次日志 self.goods_in_qty = total_new_qty - total_old_qty self.goods_out_qty = total_new_out - total_old_out self.save() # 同时更新批次统计数据 self.update_batch_stats() def update_batch_stats(self): """更新批次的统计数据""" # 聚合托盘明细数据 stats = ContainerDetailModel.objects.filter( batch=self.batch, is_delete=False ).aggregate( total_qty=Sum('goods_qty'), total_out_qty=Sum('goods_out_qty') ) # 更新批次数据 self.batch.goods_in_qty = stats['total_qty'] or 0 self.batch.goods_in_location_qty = (stats['total_qty'] or 0) - (stats['total_out_qty'] or 0) self.batch.goods_out_qty = stats['total_out_qty'] or 0 self.batch.save() # 批次日志聚合处理器 def aggregate_to_batch_log(container_log): """将托盘日志聚合到批次日志""" logger.info(f"开始聚合托盘日志: {container_log.id}") # 检查日志是否已处理 if container_log.tobatchlog: logger.info(f"托盘日志 {container_log.id} 已处理过,跳过") return try: # 确定批次 detail = container_log.container_detail if not detail: logger.warning(f"托盘日志 {container_log.id} 缺少关联的托盘明细") return batch = detail.batch if not batch: logger.warning(f"托盘明细 {detail.id} 缺少关联的批次") return # 确定操作类型 log_type = container_log.log_type # 创建或获取批次日志(按类型和时间窗口分组) # 设置时间窗口(5分钟) time_window_start = container_log.create_time - timedelta(minutes=2) time_window_end = container_log.create_time + timedelta(minutes=2) # 查找相同批次、相同类型、在时间窗口内的批次日志 existing_logs = batchLogModel.objects.filter( batch=batch, log_type=log_type, create_time__gte=time_window_start, create_time__lte=time_window_end ).order_by('create_time') if existing_logs.exists(): # 使用时间窗口内最早的批次日志 batch_log = existing_logs.first() logger.info(f"找到已有批次日志 {batch_log.id} 用于聚合") else: # 创建新的批次日志 batch_log = batchLogModel.objects.create( batch=batch, log_type=log_type, goods_code=batch.goods_code, goods_desc=batch.goods_desc, create_time=container_log.create_time ) logger.info(f"创建新批次日志 {batch_log.id}") # 将托盘日志关联到批次日志 batch_log.detail_logs.add(container_log) # 从关联日志更新批次日志数据 batch_log.update_from_details() # 根据日志类型添加关联单据信息 if log_type == 'out' and not batch_log.bound: from bound.models import OutBoundDetailModel bound = OutBoundDetailModel.objects.filter( bound_batch_number=batch ).order_by('-create_time').first() if bound: batch_log.bound = bound.bound_list batch_log.save() logger.info(f"为批次日志 {batch_log.id} 添加出库单关联") if log_type == 'create' and not batch_log.bound: from bound.models import BoundDetailModel bound = BoundDetailModel.objects.filter( bound_batch=batch ).order_by('-create_time').first() if bound: batch_log.bound = bound.bound_list logger.info(f"为批次日志 {batch_log.id} 添加入库单关联") batch_log.save() # 标记日志已处理 container_log.tobatchlog = True container_log.save() logger.info(f"成功聚合托盘日志 {container_log.id} 到批次日志 {batch_log.id}") return batch_log except Exception as e: logger.error(f"聚合托盘日志时出错: {e}", exc_info=True) raise # 简化的信号处理器 @receiver(post_save, sender=ContainerDetailLogModel) def handle_container_detail_log(sender, instance, created, **kwargs): """创建托盘日志后立即关联到批次日志""" if created: try: aggregate_to_batch_log(instance) except Exception as e: print(f"Error aggregating log: {e}") @receiver(pre_save, sender=ContainerDetailModel) def container_detail_pre_save(sender, instance, **kwargs): """在托盘明细保存前记录变更""" if instance.pk: try: old_instance = ContainerDetailModel.objects.get(pk=instance.pk) logs = [] # 数量变化日志 if old_instance.goods_qty != instance.goods_qty: logs.append(ContainerDetailLogModel( container_detail=instance, log_type='update', old_goods_qty=old_instance.goods_qty, new_goods_qty=instance.goods_qty, creater=instance.creater )) # 出库数量变化日志 if old_instance.goods_out_qty != instance.goods_out_qty: if old_instance.goods_out_qty < instance.goods_out_qty: log_type = 'out' else: log_type = 'cancel_out' logs.append(ContainerDetailLogModel( container_detail=instance, log_type=log_type, old_goods_out_qty=old_instance.goods_out_qty, new_goods_out_qty=instance.goods_out_qty, creater=instance.creater )) # 删除日志 if old_instance.is_delete != instance.is_delete and instance.is_delete: logs.append(ContainerDetailLogModel( container_detail=instance, log_type='delete', old_goods_qty=old_instance.goods_qty, old_goods_out_qty=old_instance.goods_out_qty, new_goods_qty = old_instance.goods_qty, new_goods_out_qty = old_instance.goods_qty, old_status=old_instance.status, creater=instance.creater )) # 批量创建日志 if logs: created_logs = ContainerDetailLogModel.objects.bulk_create(logs) for log in created_logs: # 由于bulk_create不会触发信号,我们手动调用信号处理函数 handle_container_detail_log(ContainerDetailLogModel, log, created=True) except ContainerDetailModel.DoesNotExist: pass @receiver(post_save, sender=ContainerDetailModel) def container_detail_post_save(sender, instance, created, **kwargs): """在托盘明细新建时创建日志""" if created: ContainerDetailLogModel.objects.create( container_detail=instance, log_type='create', creater=instance.creater, new_goods_qty=instance.goods_qty, new_status=instance.status ) # 明细表:操作记录 记录每次出入库的记录,使用goods来进行盘点,使用托盘码来进行托盘的操作记录 class ContainerOperationModel(models.Model): OPERATION_TYPES = ( ('container','组盘'), ('inbound', '入库'), ('outbound', '出库'), ('adjust', '调整'), ) month = models.IntegerField(verbose_name='月份') container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='operations') operation_type = models.CharField(max_length=20, choices=OPERATION_TYPES, verbose_name='操作类型') bound_id = models.IntegerField(verbose_name='出库申请', null=True, blank=True) batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='批次',null=True, blank=True) goods_code = models.CharField(max_length=50, verbose_name='货品编码') goods_desc = models.CharField(max_length=100, verbose_name='货品描述') goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量') goods_weight = models.DecimalField(max_digits=10, decimal_places=3, verbose_name='重量') operator = models.CharField(max_length=50, verbose_name='操作人') timestamp = models.DateTimeField(auto_now_add=True, verbose_name='操作时间') from_location = models.CharField(max_length=50, null=True, verbose_name='原库位') to_location = models.CharField(max_length=50, null=True, verbose_name='目标库位') memo = models.TextField(null=True, verbose_name='备注') is_delete = models.BooleanField(default=False, verbose_name='是否删除') class Meta: db_table = 'container_operation' verbose_name = 'ContainerOperation' verbose_name_plural = "ContainerOperation" ordering = ['-timestamp'] class ContainerWCSModel(models.Model): TASK_STATUS = ( (100, '等待中'), (101, '处理中'), (102, '已暂停'), (103, '入库中'), (200, '已发送'), (300, '已完成'), (400, '失败'), ) taskid = models.CharField(max_length=50, verbose_name='任务ID') batch = models.ForeignKey(BoundBatchModel, on_delete=models.CASCADE, verbose_name='关联批次',null=True, blank=True ) batch_out = models.ForeignKey(OutBatchModel, on_delete=models.CASCADE, verbose_name='出库批次' , null=True, blank=True ) bound_list = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, verbose_name='关联出库单',null=True, blank=True ) batch_number = models.CharField(max_length=50, verbose_name='批次号',null=True, blank=True ) sequence = models.BigIntegerField(verbose_name='任务顺序') priority = models.IntegerField(default=100, verbose_name='优先级') month = models.IntegerField(verbose_name='月份') tasktype = models.CharField(max_length=50, verbose_name='任务类型') tasknumber = models.IntegerField(verbose_name='任务号',unique=True) order_number = models.IntegerField(verbose_name='c_number') container = models.CharField(max_length=50, verbose_name='托盘号') current_location = models.CharField(max_length=50, verbose_name='当前库位') target_location = models.CharField(max_length=50, verbose_name='目标库位') message = models.TextField(verbose_name='消息') working = models.IntegerField(default = 1,verbose_name='工作状态') status = models.IntegerField(choices=TASK_STATUS, default=100, verbose_name='状态') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') is_delete = models.BooleanField(default=False, verbose_name='是否删除') class Meta: db_table = 'container_wcs' verbose_name = 'ContainerWCS' verbose_name_plural = "ContainerWCS" ordering = ['-create_time'] def to_dict(self): return { 'container': self.container, 'current_location': self.current_location, 'month' : self.month, 'target_location': self.target_location, 'tasktype': self.tasktype, 'taskid': self.taskid, 'taskNumber': self.tasknumber-20000000000, 'message': self.message, 'container': self.container, 'status': self.status } def __str__(self): return f"{self.taskid} - {self.get_status_display()}" # 这里的批次详情是主入库申请单下的子批次 class TaskModel(models.Model): task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, related_name='tasks') batch_detail = models.ForeignKey(BoundDetailModel, on_delete=models.CASCADE, verbose_name='批次详情') container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细') class Meta: db_table = 'task' verbose_name = 'Task' verbose_name_plural = "Task" ordering = ['-id'] class out_batch_detail(models.Model): out_bound = models.ForeignKey(BoundListModel, on_delete=models.CASCADE, related_name='out_batch_details') container = models.ForeignKey(ContainerListModel, on_delete=models.CASCADE, related_name='out_batch_details') container_detail = models.ForeignKey(ContainerDetailModel, on_delete=models.CASCADE, verbose_name='托盘明细') out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='数量') last_out_goods_qty = models.DecimalField(max_digits=10, decimal_places=3, default=Decimal('0'),verbose_name='上次出库数量') working = models.IntegerField(default = 1,verbose_name='工作状态') is_delete = models.BooleanField(default=False, verbose_name='是否删除') class Meta: db_table = 'out_batch_detail' verbose_name = 'OutBatchDetail' verbose_name_plural = "OutBatchDetail" ordering = ['container']