forked from erp-dev/erp
220 lines
7.3 KiB
Python
220 lines
7.3 KiB
Python
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
from flower.common import ModelBase
|
|
|
|
|
|
class MissionPayloadProcessorEnum(models.TextChoices):
|
|
STRUCTURED_DESCRIPTION_V1 = "structured_description_v1", "结构化描述增强(v1)"
|
|
|
|
|
|
class MissionCategory(ModelBase):
|
|
"""任务分类字典,按商户隔离。"""
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
merchant = models.ForeignKey(
|
|
"basic_info.Merchant",
|
|
on_delete=models.PROTECT,
|
|
related_name="mission_categories",
|
|
verbose_name="所属商户",
|
|
)
|
|
name = models.CharField(max_length=50, verbose_name="分类名称")
|
|
payload_processor = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
default="",
|
|
choices=MissionPayloadProcessorEnum.choices,
|
|
verbose_name="payload 增强器",
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "任务分类"
|
|
verbose_name_plural = "任务分类"
|
|
ordering = ["id"]
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["merchant", "name"], name="unique_mission_category_name_per_merchant"),
|
|
]
|
|
|
|
|
|
class Mission(ModelBase):
|
|
"""中立任务,可选关联到系统中的任意业务对象。"""
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
merchant = models.ForeignKey(
|
|
"basic_info.Merchant",
|
|
on_delete=models.PROTECT,
|
|
related_name="missions",
|
|
verbose_name="所属商户",
|
|
)
|
|
description = models.TextField(verbose_name="任务描述")
|
|
category = models.ForeignKey(
|
|
MissionCategory,
|
|
on_delete=models.PROTECT,
|
|
related_name="missions",
|
|
verbose_name="任务类型",
|
|
)
|
|
is_urgent = models.BooleanField(default=False, db_index=True, verbose_name="是否紧急")
|
|
is_completed = models.BooleanField(default=False, db_index=True, verbose_name="是否完成")
|
|
is_cancelled = models.BooleanField(default=False, db_index=True, verbose_name="是否取消")
|
|
notify_if_unreplied = models.BooleanField(default=False, db_index=True, verbose_name="是否开启未回复提醒")
|
|
unreplied_notify_interval_minutes = models.PositiveIntegerField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="未回复提醒间隔(分钟)",
|
|
)
|
|
unreplied_notify_max_count = models.PositiveIntegerField(default=5, verbose_name="未回复最大提醒次数")
|
|
unreplied_notify_sent_count = models.PositiveIntegerField(default=0, verbose_name="未回复已提醒次数")
|
|
unreplied_last_notified_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="上一次未回复提醒时间",
|
|
)
|
|
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name="取消时间")
|
|
creator = models.ForeignKey(
|
|
"basic_info.Employee",
|
|
on_delete=models.PROTECT,
|
|
related_name="created_missions",
|
|
verbose_name="任务创建者",
|
|
)
|
|
cancelled_by = models.ForeignKey(
|
|
"basic_info.Employee",
|
|
on_delete=models.PROTECT,
|
|
related_name="cancelled_missions",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="取消人",
|
|
)
|
|
content_type = models.ForeignKey(
|
|
ContentType,
|
|
on_delete=models.SET_NULL,
|
|
related_name="missions",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="关联对象类型",
|
|
)
|
|
content_id = models.PositiveBigIntegerField(null=True, blank=True, db_index=True, verbose_name="关联对象ID")
|
|
extra = models.JSONField(null=True, blank=True, verbose_name="扩展字段")
|
|
content_object = GenericForeignKey("content_type", "content_id")
|
|
|
|
@property
|
|
def has_ending_reply(self) -> bool:
|
|
return self.replies.filter(ends_task=True, is_rejected=False).exists()
|
|
|
|
@property
|
|
def can_reply(self) -> bool:
|
|
return not self.is_cancelled and not self.has_ending_reply
|
|
|
|
@property
|
|
def has_effective_reply(self) -> bool:
|
|
return self.replies.filter(is_rejected=False).exists()
|
|
|
|
def get_participants(self):
|
|
return self.participants.select_related("employee")
|
|
|
|
def filter_participants(self, **filters):
|
|
return self.get_participants().filter(**filters)
|
|
|
|
@property
|
|
def category_name(self) -> str:
|
|
return self.category.name
|
|
|
|
def __str__(self):
|
|
return f"Mission #{self.id}"
|
|
|
|
class Meta:
|
|
verbose_name = "任务"
|
|
verbose_name_plural = "任务"
|
|
permissions = [
|
|
("reopen_mission", "Can reopen mission"),
|
|
("reject_mission_reply", "Can reject mission reply"),
|
|
]
|
|
indexes = [
|
|
models.Index(fields=["merchant", "content_type", "content_id"]),
|
|
models.Index(fields=["merchant", "is_completed", "is_cancelled"]),
|
|
]
|
|
|
|
|
|
class MissionParticipant(ModelBase):
|
|
"""任务参与员工。"""
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
merchant = models.ForeignKey(
|
|
"basic_info.Merchant",
|
|
on_delete=models.PROTECT,
|
|
related_name="mission_participants",
|
|
verbose_name="所属商户",
|
|
)
|
|
mission = models.ForeignKey(
|
|
Mission,
|
|
on_delete=models.CASCADE,
|
|
related_name="participants",
|
|
verbose_name="任务",
|
|
)
|
|
employee = models.ForeignKey(
|
|
"basic_info.Employee",
|
|
on_delete=models.PROTECT,
|
|
related_name="mission_participations",
|
|
verbose_name="参与者",
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"{self.mission_id} - {self.employee_id}"
|
|
|
|
class Meta:
|
|
verbose_name = "任务参与者"
|
|
verbose_name_plural = "任务参与者"
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["mission", "employee"], name="unique_mission_participant"),
|
|
]
|
|
|
|
|
|
class MissionReply(ModelBase):
|
|
"""任务回应记录。"""
|
|
|
|
id = models.BigAutoField(primary_key=True)
|
|
merchant = models.ForeignKey(
|
|
"basic_info.Merchant",
|
|
on_delete=models.PROTECT,
|
|
related_name="mission_replies",
|
|
verbose_name="所属商户",
|
|
)
|
|
mission = models.ForeignKey(
|
|
Mission,
|
|
on_delete=models.CASCADE,
|
|
related_name="replies",
|
|
verbose_name="任务",
|
|
)
|
|
responder = models.ForeignKey(
|
|
"basic_info.Employee",
|
|
on_delete=models.PROTECT,
|
|
related_name="mission_replies",
|
|
verbose_name="回应者",
|
|
)
|
|
content = models.TextField(verbose_name="回应内容")
|
|
extra = models.JSONField(null=True, blank=True, verbose_name="扩展字段")
|
|
replied_at = models.DateTimeField(default=timezone.now, db_index=True, verbose_name="回应时间")
|
|
ends_task = models.BooleanField(default=False, db_index=True, verbose_name="是否结束任务")
|
|
is_rejected = models.BooleanField(default=False, db_index=True, verbose_name="是否被撤销")
|
|
rejected_by = models.ForeignKey(
|
|
"basic_info.Employee",
|
|
on_delete=models.PROTECT,
|
|
related_name="rejected_mission_replies",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name="撤销人",
|
|
)
|
|
rejected_at = models.DateTimeField(null=True, blank=True, verbose_name="撤销时间")
|
|
|
|
def __str__(self):
|
|
return f"MissionReply #{self.id}"
|
|
|
|
class Meta:
|
|
verbose_name = "任务回应"
|
|
verbose_name_plural = "任务回应"
|
|
ordering = ["replied_at", "id"]
|