1
0
forked from erp-dev/erp
Files
erpnew/printing/models.py
2026-01-06 12:05:23 +08:00

497 lines
19 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):
"""开版管理订单"""
# 自动编号相关
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', '可以恢复开版订单'),
]
@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 PrintingOrder(ModelBase):
"""PrintingOrder model representing a printing order."""
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='印染流程',
)
def __str__(self):
return self.human_id
class Meta:
verbose_name = '印染订单'
verbose_name_plural = '印染订单'
permissions = [
('can_invalidate_printingorder', '可以作废印染订单'),
('can_activate_printingorder', '可以恢复印染订单'),
]
@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."""
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='流程实例',
)
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本次被“完成”的流程节点即推进时的 next_pending_state
- printing_jobs本次批量推进涉及的所有明细
"""
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,
)
class Meta:
verbose_name = '印染任务批量推进记录'
verbose_name_plural = '印染任务批量推进记录'