123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- 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']
|