1
0
forked from erp-dev/erp
Files
erpnew/printing/models.py

606 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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']
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.DateField(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='创建人',
)
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='业务进展',
)
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='创建人',
)
def __str__(self):
return self.printing_order.human_id
@property
def billed_quantity(self) -> Decimal:
"""
开单数量:所有关联销售明细的数量之和
"""
from business.models import SalesOrderItem
total = self.sales_order_items.aggregate(total=models.Sum('quantity')).get('total')
return total or Decimal('0')
@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 = '印染任务批量操作记录'