1
0
forked from erp-dev/erp
Files
erpnew/mission/models.py
2026-05-01 22:43:22 +08:00

209 lines
7.0 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 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="分类名称")
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"]