from decimal import Decimal from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db import models from flower.common import ModelBase from basic_info import models as basic_models from stateflow import models as stateflow_models class PlateOrder(ModelBase): """开版管理订单""" # 商户关联(多租户) merchant = models.ForeignKey( basic_models.Merchant, on_delete=models.PROTECT, null=True, blank=True, related_name='plate_orders', verbose_name='商户', help_text='所属商户,可空以兼容历史数据' ) # 自动编号相关 design_code = models.CharField(max_length=50, blank=True, null=True, verbose_name='设计编号') original_id = models.PositiveBigIntegerField( null=True, blank=True, verbose_name='克隆来源订单ID', help_text='前端克隆概念:记录本订单由哪个订单复制而来(无外键约束)', ) # 版相关信息 plate_type = models.CharField(max_length=20, blank=True, null=True, verbose_name='起版情况') # 首版/复版等 plate_date = models.DateTimeField(null=True, blank=True, verbose_name='下版时间') plate_method = models.CharField(max_length=50, blank=True, null=True, verbose_name='开版方式') plate_image = models.JSONField( default=list, blank=True, verbose_name='开版图', help_text='存储多个开版图片引用信息,例如 [{"file_id": 1, "name": "封面", "path": "/media/xxx"}]' ) image_name = models.TextField(blank=True, null=True, verbose_name='图片名称') plate_notes = models.TextField(blank=True, null=True, verbose_name='打版注意事项') reprint_reason = models.TextField(blank=True, null=True, verbose_name='复版原因') # 状态和进程 # development_status = models.CharField(max_length=20, blank=True, null=True, verbose_name='开发进程') # 未打印/待画图等 urgency_level = models.CharField(max_length=20, default='正常', verbose_name='紧急程度') # 正常/加急 # 人员关联 salesperson = models.ForeignKey( basic_models.Employee, on_delete=models.PROTECT, null=True, blank=True, related_name='plate_orders_as_salesperson', verbose_name='销售员' ) merchandiser = models.ForeignKey( basic_models.Employee, on_delete=models.PROTECT, null=True, blank=True, related_name='plate_orders_as_merchandiser', verbose_name='跟单员' ) designer = models.ForeignKey( basic_models.Employee, on_delete=models.PROTECT, null=True, blank=True, related_name='plate_orders_as_designer', verbose_name='设计师' ) # 客户和地址信息 customer = models.ForeignKey( basic_models.Customer, on_delete=models.PROTECT, related_name='plate_orders', verbose_name='客户', ) area = models.CharField(max_length=255, blank=True, null=True, verbose_name='区域') default_address = models.CharField(max_length=500, blank=True, null=True, verbose_name='默认地址') # 质量评级 is_mark_frame = models.BooleanField(default=False, verbose_name='是否套唛架') drawing_rating = models.CharField(max_length=20, blank=True, null=True, verbose_name='画图评级') color_matching_rating = models.CharField(max_length=20, blank=True, null=True, verbose_name='调色评级') sample_rating = models.CharField(max_length=20, blank=True, null=True, verbose_name='套样评级') difficulty_rating = models.CharField(max_length=20, blank=True, null=True, verbose_name='难度评级') # 时间相关 required_completion_date = models.DateTimeField(null=True, blank=True, verbose_name='要求完成时间') completion_date = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') # 产品信息 fabric = models.CharField(max_length=100, blank=True, null=True, verbose_name='布料') fabric_source = models.CharField(max_length=100, blank=True, null=True, verbose_name='布料来源') width = models.CharField(max_length=50, blank=True, null=True, verbose_name='幅宽') style_name = models.CharField(max_length=100, blank=True, null=True, verbose_name='款号名称') # 生产方式 production_method = models.CharField(max_length=50, blank=True, null=True, verbose_name='做货方式') # 样品相关 sample_meter = models.CharField(max_length=100, blank=True, null=True, verbose_name='米样') required_sample_meters = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='客户要求米样米数' ) # 审批和订单状态 approval_result = models.CharField(max_length=50, blank=True, null=True, verbose_name='审批结果') is_ordered = models.BooleanField(default=False, verbose_name='是否已下单') is_invalid = models.BooleanField(default=False, verbose_name='是否作废') customer_feedback = models.TextField(blank=True, null=True, verbose_name='客户修改意见') print_count = models.PositiveIntegerField(default=0, verbose_name='打印次数') # 流程管理 process = models.IntegerField(default=settings.PLATE_ORDER_DEFAULT_PROCESS_ID, verbose_name='关联流程') created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_plate_orders', verbose_name='创建人', ) business_object = models.OneToOneField( stateflow_models.BusinessObject, on_delete=models.SET_NULL, null=True, blank=True, related_name='plate_order', verbose_name='流程实例', ) def __str__(self): return f'PlateOrder-{self.id}' def save(self, *args, **kwargs): """保存时自动创建 BusinessObject""" # 先保存以获取 ID(如果是新建) is_new = self.pk is None update_fields = kwargs.get('update_fields') super().save(*args, **kwargs) should_auto_create = ( self.process and not self.business_object and (is_new or update_fields is None or 'process' in (update_fields or [])) ) # 如果有 process 且没有 business_object,则自动创建 if should_auto_create: try: from stateflow.models import Process process_obj = Process.objects.get(id=self.process) # 创建 BusinessObject business_object = stateflow_models.BusinessObject.objects.create( name=f"PlateOrder-{self.id}", process=process_obj, description=f"开版订单 {self.id} 的流程实例", content_type=ContentType.objects.get_for_model(PlateOrder), object_id=self.id, ) self.business_object = business_object # 再次保存以更新 business_object 字段 super().save(update_fields=['business_object']) except Process.DoesNotExist: # 如果流程不存在,忽略错误 pass class Meta: verbose_name = '开版订单' verbose_name_plural = '开版订单' ordering = ['-created_at'] indexes = [ models.Index( fields=['merchant', 'plate_date'], name='idx_plateord_merch_pltdt', ), ] permissions = [ ('can_invalidate_plateorder', '可以作废开版订单'), ('can_activate_plateorder', '可以恢复开版订单'), ('view_all_plateorders', '查看所有开版订单(突破客户可见性限制)'), ] @property def status(self) -> str: """ 返回当前状态名称 规则: - current_state 为 None:返回"已完成" - 否则:返回 current_state 的名称 注意:废除了"未开始"的概念,直接显示节点名称 """ from stateflow.services import get_display_status return get_display_status(self.business_object) @property def last_completed_state(self) -> str: """ 返回最后一个已完成节点的名称,用于详情展示 """ if not self.business_object: return '' from stateflow.services import get_last_completed_state last_state = get_last_completed_state(self.business_object) return last_state.name if last_state else '' @property def status_id(self): """返回当前状态ID(下一个待执行的状态ID)""" if not self.business_object: return None current_state = self.business_object.get_current_state() return current_state.id if current_state else None @property def is_completed(self) -> bool: """判断是否完成""" if not self.business_object: return False # 检查流程是否完成 from stateflow.services import get_overall_status return get_overall_status(self.business_object) == 'completed' @property def has_started(self) -> bool: """判断是否已开始(有未撤销的状态记录)""" if not self.business_object: return False # 只检查未撤销的记录 from stateflow.models import StateFlowRecord has_records = StateFlowRecord.objects.filter( business_object=self.business_object, is_cancelled=False ).exists() return has_records @property def progress_percentage(self) -> float: """计算进度百分比""" if not self.business_object: return 0.0 return self.business_object.get_progress_percentage() class PlateOrderTiiaUploadFailure(ModelBase): """ PlateOrder 图片上传腾讯云图库失败记录(用于定时任务/人工重试追踪)。 设计目标:最小可用——记录“哪天跑的任务 + 哪个 plate_order_id + 失败详情/次数”。 """ run_date = models.DateField( verbose_name='任务日期', help_text='以任务处理的日期为准(通常为“昨天”)', ) plate_order_id = models.PositiveBigIntegerField( verbose_name='开版订单ID', db_index=True, help_text='PlateOrder.id(不使用外键,避免历史数据/删除带来的约束问题)', ) error = models.TextField( blank=True, default='', verbose_name='错误摘要', ) details = models.JSONField( default=list, blank=True, verbose_name='错误详情', help_text='通常存储 upload_plate_order_images_to_tencent_tiia 返回的失败条目列表', ) attempts = models.PositiveIntegerField( default=0, verbose_name='尝试次数', ) last_attempt_at = models.DateTimeField( null=True, blank=True, verbose_name='最后尝试时间', ) class Meta: verbose_name = '开版订单图片上传失败记录' verbose_name_plural = '开版订单图片上传失败记录' ordering = ['-created_at'] constraints = [ models.UniqueConstraint( fields=['run_date', 'plate_order_id'], name='uniq_plateorder_tiia_fail_run_date_po', ) ] class PrintingOrder(ModelBase): """PrintingOrder model representing a printing order.""" # 商户关联(多租户) merchant = models.ForeignKey( basic_models.Merchant, on_delete=models.PROTECT, null=True, blank=True, related_name='printing_orders', verbose_name='商户', help_text='所属商户,可空以兼容历史数据' ) customer = models.ForeignKey( basic_models.Customer, on_delete=models.PROTECT, related_name='printing_orders', verbose_name='客户', ) fabric = models.CharField(max_length=100, verbose_name='面料') width = models.CharField(max_length=100, verbose_name='幅宽') is_urgent = models.BooleanField(default=False, verbose_name='紧急') area = models.CharField(max_length=100, null=True, blank=True, verbose_name='地区') address = models.CharField(max_length=200, null=True, blank=True, verbose_name='地址') fabric_source = models.CharField(max_length=100, null=True, blank=True, verbose_name='布料来源') is_fabric_received = models.BooleanField(default=False, verbose_name='布料是否已收') craft = models.CharField(max_length=100, null=True, blank=True, verbose_name='工艺') description = models.TextField(blank=True, null=True, verbose_name='订单描述') outgoing_date = models.DateTimeField(null=True, blank=True, verbose_name='出货日期') curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='曲线') new_curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='新加曲线') position = models.TextField(blank=True, null=True, verbose_name='位置') printing_warn = models.TextField(blank=True, null=True, verbose_name='打印注意事项') rolling_warn = models.TextField(blank=True, null=True, verbose_name='滚筒注意事项') production_warn = models.TextField(blank=True, null=True, verbose_name='生产注意事项') print_count = models.PositiveIntegerField(default=0, verbose_name='打印次数') is_invalid = models.BooleanField(default=False, verbose_name='是否作废') process = models.ForeignKey( stateflow_models.Process, on_delete=models.PROTECT, null=True, blank=True, related_name='printing_orders', verbose_name='印染流程', ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_orders', verbose_name='创建人', ) external_order_id = models.CharField( max_length=100, blank=True, null=True, db_index=True, verbose_name='外部订单编号', ) external_customer_id = models.CharField( max_length=100, blank=True, null=True, verbose_name='外部客户ID', ) external_customer_name = models.CharField( max_length=100, blank=True, null=True, verbose_name='外部客户名', ) external_employee_name = models.CharField( max_length=100, blank=True, null=True, verbose_name='外部员工名', ) external_raw = models.JSONField( default=dict, blank=True, verbose_name='外部原始数据', help_text='保存外部 records 聚合后的 order 级原始数据快照', ) def __str__(self): return self.human_id class Meta: verbose_name = '印染订单' verbose_name_plural = '印染订单' permissions = [ ('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单'), ('view_all_printingorders', '查看所有印染订单(突破客户可见性限制)'), ] @property def human_id(self) -> str: if self.id is None or self.created_at is None: return "" return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}" def can_change_process(self) -> bool: """检查是否可以修改流程(所有job都未开始)""" return not any(job.has_started for job in self.printing_jobs.all()) @property def progress(self) -> int: """计算订单完成进度百分比(已完成的任务数 / 总任务数)""" jobs = self.printing_jobs.all() if not jobs.exists(): return 0 completed_count = sum(1 for job in jobs if job.is_completed) return int((completed_count / jobs.count()) * 100) class PrintingJobWorkStateEnum(models.IntegerChoices): PRODUCING = 0, '生产中' WAITING_FOR_DELIVERY = 1, '待送货' WAITING_FOR_INVOICE = 2, '待开单' FINISHED = 3, '已完结' class PrintingJob(ModelBase): """PrintingJob model representing a printing job associated with an order.""" # 商户关联(多租户) merchant = models.ForeignKey( basic_models.Merchant, on_delete=models.PROTECT, null=True, blank=True, related_name='printing_jobs', verbose_name='商户', help_text='所属商户,可空以兼容历史数据' ) printing_order = models.ForeignKey( PrintingOrder, on_delete=models.CASCADE, related_name='printing_jobs', verbose_name='主订单', ) product = models.ForeignKey( basic_models.Product, on_delete=models.PROTECT, related_name='printing_jobs', verbose_name='产品', ) work_state = models.IntegerField( choices=PrintingJobWorkStateEnum.choices, default=PrintingJobWorkStateEnum.PRODUCING, verbose_name='业务进展', ) is_production_completed = models.BooleanField( default=False, verbose_name='是否完成生产', help_text='独立于流程计算状态的生产完成标记', ) quantity = models.PositiveIntegerField(verbose_name='数量') unit = models.CharField(max_length=50, verbose_name='单位') size = models.CharField(max_length=100, null=True, blank=True, verbose_name='一段尺寸') pieces = models.PositiveIntegerField(null=True, blank=True, verbose_name='件数') description = models.TextField(blank=True, null=True, verbose_name='备注') original_id = models.PositiveBigIntegerField( null=True, blank=True, verbose_name='克隆来源任务ID', help_text='前端克隆概念:记录本任务由哪个任务复制而来(无外键约束)', ) business_object = models.OneToOneField( stateflow_models.BusinessObject, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_job', verbose_name='流程实例', ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_jobs', verbose_name='创建人', ) external_product_name = models.CharField( max_length=200, blank=True, null=True, verbose_name='外部产品名', ) external_raw = models.JSONField( default=dict, blank=True, verbose_name='外部原始数据', help_text='保存外部 record 的完整原始数据', ) def __str__(self): return self.printing_order.human_id @property def billed_quantity(self) -> Decimal: """ 开单数量:所有关联的非作废销售单明细数量之和 """ from business.models import SalesOrderStatusEnum total = self.sales_order_items.exclude( sales_order__status=SalesOrderStatusEnum.CANCELLED, ).aggregate(total=models.Sum('quantity')).get('total') return total or Decimal('0') @property def is_sales_order_bound(self) -> bool: """是否已被非作废销售单明细绑定。""" from business.models import SalesOrderStatusEnum cached = getattr(self, '_is_sales_order_bound_cache', None) if cached is not None: return bool(cached) return self.sales_order_items.exclude( sales_order__status=SalesOrderStatusEnum.CANCELLED, ).exists() @property def status(self) -> str: """ 返回当前状态名称 规则: - current_state 为 None:返回"已完成" - 否则:返回 current_state 的名称 注意:废除了"未开始"的概念,直接显示节点名称 """ from stateflow.services import get_display_status return get_display_status(self.business_object) @property def status_id(self): """返回当前状态ID(下一个待执行的状态ID)""" if not self.business_object: return None current_state = self.business_object.get_current_state() return current_state.id if current_state else None @property def is_completed(self) -> bool: """判断是否完成(所有流程节点都已完成)""" if not self.business_object or not self.printing_order.process: return False # 获取流程的所有节点 process_nodes = self.printing_order.process.process_nodes.all() total_nodes = process_nodes.count() if total_nodes == 0: return False # 检查已完成的节点数 completed_count = self.business_object.state_logs.filter(is_cancelled=False).count() # 所有节点都完成了才算完成 return completed_count >= total_nodes @property def has_started(self) -> bool: """判断是否已开始(有未撤销的状态记录)""" if not self.business_object: return False # 只检查未撤销的记录 from stateflow.models import StateFlowRecord has_records = StateFlowRecord.objects.filter( business_object=self.business_object, is_cancelled=False ).exists() return has_records @property def progress_percentage(self) -> float: """计算进度百分比""" if not self.business_object: return 0.0 return self.business_object.get_progress_percentage() @property def last_completed_state(self) -> str: """返回最后一个已完成节点的名称,用于详情展示""" if not self.business_object: return '' from stateflow.services import get_last_completed_state last_state = get_last_completed_state(self.business_object) return last_state.name if last_state else '' class Meta: verbose_name = '印染款式明细' verbose_name_plural = '印染款式明细' class PrintingJobBatchAdvanceRecord(ModelBase): """ 印染任务批量操作记录(审计) 记录一次"对多个 PrintingJob 用同一组参数操作同一步流程节点"的行为: - created_by/created_at:谁在何时做了批量操作 - parameters:本次操作提交的工艺参数(JSON) - state:本次操作的流程节点 - printing_jobs:本次批量操作涉及的所有明细 - only_parameters:操作类型 - False(默认):批量推进状态 - True:仅批量补充工艺参数(不推进状态) """ printing_order = models.ForeignKey( PrintingOrder, on_delete=models.PROTECT, related_name='batch_advance_records', verbose_name='印染订单', ) state = models.ForeignKey( stateflow_models.State, on_delete=models.PROTECT, related_name='printing_job_batch_advance_records', verbose_name='操作的流程节点', ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_job_batch_advance_records', verbose_name='创建人', ) parameters = models.JSONField( default=dict, blank=True, verbose_name='工艺参数', help_text='批量操作时提交的参数', ) printing_jobs = models.ManyToManyField( PrintingJob, related_name='batch_advance_records', verbose_name='印染明细', blank=False, ) only_parameters = models.BooleanField( default=False, verbose_name='仅参数补充', help_text='True=仅补充参数(不推进状态),False=批量推进状态', ) class Meta: verbose_name = '印染任务批量操作记录' verbose_name_plural = '印染任务批量操作记录' class PrintingOrderExternalSnapshotSyncAudit(ModelBase): """ 外部订单快照同步审计记录。 记录一次“按 external_order_id 触发外部快照覆盖同步”的执行结果: - before_snapshot: 同步前本地 PrintingOrder / PrintingJob 快照 - operator_user / operator_employee: 触发人 - is_success / failure_reason: 成功或失败及失败原因 - allow_reset_stateflow: 本次是否允许撤销已有工序 """ printing_order = models.ForeignKey( PrintingOrder, on_delete=models.SET_NULL, null=True, blank=True, related_name='external_snapshot_sync_audits', verbose_name='印染订单', ) external_order_id = models.CharField( max_length=100, db_index=True, verbose_name='外部订单编号', ) operator_user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_order_external_snapshot_sync_audits', verbose_name='操作者', ) operator_employee = models.ForeignKey( basic_models.Employee, on_delete=models.SET_NULL, null=True, blank=True, related_name='printing_order_external_snapshot_sync_audits', verbose_name='操作员工', ) before_snapshot = models.JSONField( default=dict, blank=True, verbose_name='同步前快照', ) allow_reset_stateflow = models.BooleanField( default=False, verbose_name='允许撤销工序', ) is_success = models.BooleanField( default=False, verbose_name='是否成功', ) failure_reason = models.TextField( blank=True, default='', verbose_name='失败原因', ) class Meta: verbose_name = '外部订单快照同步审计记录' verbose_name_plural = '外部订单快照同步审计记录' ordering = ['-created_at', '-id'] def __str__(self) -> str: return f'{self.external_order_id} #{self.id}'