156 lines
7.4 KiB
Python
156 lines
7.4 KiB
Python
from django.conf import settings
|
|
from django.contrib.auth.models import Permission
|
|
from django.db import models
|
|
from apps.core.models import PersianTextNormalizationMixin, TimeStampedModel
|
|
|
|
|
|
class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
|
"""A business unit whose access rules are managed from Django Admin."""
|
|
|
|
persian_text_fields = {"name": False, "description": True, "search_keywords": True}
|
|
name = models.CharField(max_length=120, unique=True)
|
|
slug = models.SlugField(max_length=80, unique=True)
|
|
description = models.TextField(blank=True)
|
|
search_keywords = models.TextField(blank=True, verbose_name="کلیدواژههای جستجو", help_text="واژهها و عبارتهای مترادف را با فاصله یا ویرگول وارد کنید.")
|
|
sort_order = models.PositiveIntegerField(default=0, db_index=True, verbose_name="ترتیب نمایش")
|
|
permissions = models.ManyToManyField(
|
|
Permission,
|
|
blank=True,
|
|
related_name="organizational_units",
|
|
help_text="مجوزهایی که به تمام اعضای فعال این واحد داده میشود.",
|
|
)
|
|
is_active = models.BooleanField(default=True)
|
|
accepts_external_tickets = models.BooleanField(
|
|
default=True,
|
|
verbose_name="قابل انتخاب برای تیکت مشتری",
|
|
help_text="اگر غیرفعال باشد، مشتریان نمیتوانند مستقیماً برای این واحد تیکت ثبت کنند.",
|
|
)
|
|
show_in_customer_form = models.BooleanField(default=True, verbose_name="نمایش در فرم مشتری", help_text="در صورت خاموش بودن، واحد در فرم ثبت تیکت و جستجوی مشتری نمایش داده نمیشود.")
|
|
|
|
class Meta:
|
|
ordering = ["sort_order", "name", "pk"]
|
|
verbose_name = "واحد سازمانی"
|
|
verbose_name_plural = "واحدهای سازمانی"
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class UnitMembership(TimeStampedModel):
|
|
"""Dynamic per-user unit membership with optional extra permissions."""
|
|
|
|
class TicketScope(models.TextChoices):
|
|
SELF = "SELF", "عمومی، ایجادشده یا تخصیصیافته به خود"
|
|
UNIT = "UNIT", "تمام تیکتهای واحد"
|
|
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="unit_memberships",
|
|
)
|
|
unit = models.ForeignKey(
|
|
OrganizationalUnit,
|
|
on_delete=models.CASCADE,
|
|
related_name="memberships",
|
|
)
|
|
permissions = models.ManyToManyField(
|
|
Permission,
|
|
blank=True,
|
|
related_name="unit_memberships",
|
|
help_text="مجوزهای اضافه مخصوص این عضو در همین واحد.",
|
|
)
|
|
is_primary = models.BooleanField(default=False)
|
|
is_active = models.BooleanField(default=True)
|
|
ticket_scope = models.CharField(
|
|
max_length=12,
|
|
choices=TicketScope.choices,
|
|
default=TicketScope.SELF,
|
|
help_text="دامنه مشاهده تیکتها در همین واحد؛ مجوز سراسری مستقل است.",
|
|
)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["user", "unit"], name="unique_unit_membership")
|
|
]
|
|
ordering = ["unit__name", "user__username"]
|
|
verbose_name = "عضویت واحد"
|
|
verbose_name_plural = "عضویتهای واحد"
|
|
|
|
def __str__(self):
|
|
return f"{self.user} — {self.unit}"
|
|
|
|
|
|
class StaffProfile(PersianTextNormalizationMixin, TimeStampedModel):
|
|
class Role(models.TextChoices):
|
|
SALES_EXPERT = "SALES_EXPERT", "کارشناس فروش"
|
|
SALES_SUPERVISOR = "SALES_SUPERVISOR", "سرپرست فروش"
|
|
SALES_MANAGER = "SALES_MANAGER", "مدیر فروش"
|
|
WAREHOUSE_EXPERT = "WAREHOUSE_EXPERT", "کارشناس بارگیری انبار"
|
|
FINANCE_EXPERT = "FINANCE_EXPERT", "کارشناس مالی / حسابداری"
|
|
CEO = "CEO", "مدیرعامل"
|
|
GENERAL_ADMIN = "GENERAL_ADMIN", "مدیرکل Admin"
|
|
|
|
ROLE_GROUPS = {
|
|
Role.SALES_EXPERT: "Sales Expert",
|
|
Role.SALES_SUPERVISOR: "Sales Supervisor",
|
|
Role.SALES_MANAGER: "Sales Manager",
|
|
Role.WAREHOUSE_EXPERT: "Warehouse Expert",
|
|
Role.FINANCE_EXPERT: "Finance Expert",
|
|
Role.CEO: "CEO",
|
|
Role.GENERAL_ADMIN: "General Admin",
|
|
}
|
|
persian_text_fields = {"first_name": False, "last_name": False, "display_name": False, "job_title": False}
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="staff_profile")
|
|
primary_unit = models.ForeignKey(
|
|
OrganizationalUnit,
|
|
on_delete=models.PROTECT,
|
|
related_name="primary_staff",
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
role = models.CharField(max_length=24, choices=Role.choices, default=Role.SALES_EXPERT, db_index=True)
|
|
display_name = models.CharField(max_length=150)
|
|
first_name = models.CharField(max_length=100, blank=True)
|
|
last_name = models.CharField(max_length=100, blank=True)
|
|
job_title = models.CharField(max_length=150, blank=True, default="کارشناس فروش")
|
|
avatar = models.ImageField(upload_to="staff/avatars/", blank=True)
|
|
mobile = models.CharField(max_length=16, blank=True)
|
|
bale_chat_id = models.CharField(max_length=100, blank=True)
|
|
bale_notifications_enabled = models.BooleanField(default=True)
|
|
is_customer_selectable = models.BooleanField(default=True)
|
|
sort_order = models.PositiveIntegerField(
|
|
default=0,
|
|
db_index=True,
|
|
verbose_name="ترتیب نمایش در انتخاب کارشناس",
|
|
help_text="عدد کمتر، کارشناس را زودتر در فهرست همان واحد نمایش میدهد.",
|
|
)
|
|
is_active = models.BooleanField(default=True)
|
|
class Meta: ordering = ["sort_order", "display_name"]
|
|
def __str__(self): return self.display_name
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
group_name = self.ROLE_GROUPS[self.role]
|
|
managed_names = tuple(self.ROLE_GROUPS.values())
|
|
self.user.groups.remove(*self.user.groups.filter(name__in=managed_names))
|
|
from django.contrib.auth.models import Group
|
|
group, _ = Group.objects.get_or_create(name=group_name)
|
|
self.user.groups.add(group)
|
|
if self.primary_unit_id:
|
|
membership, membership_created = UnitMembership.objects.get_or_create(
|
|
user=self.user,
|
|
unit=self.primary_unit,
|
|
defaults={"is_primary": True},
|
|
)
|
|
desired_scope = (
|
|
UnitMembership.TicketScope.UNIT
|
|
if self.role in {self.Role.SALES_SUPERVISOR, self.Role.SALES_MANAGER, self.Role.WAREHOUSE_EXPERT, self.Role.FINANCE_EXPERT, self.Role.CEO, self.Role.GENERAL_ADMIN}
|
|
else UnitMembership.TicketScope.SELF
|
|
)
|
|
if not membership.is_primary or not membership.is_active or (membership_created and membership.ticket_scope != desired_scope):
|
|
membership.is_primary = True
|
|
membership.is_active = True
|
|
if membership_created:
|
|
membership.ticket_scope = desired_scope
|
|
membership.save(update_fields=["is_primary", "is_active", "ticket_scope", "updated_at"])
|
|
self.user.unit_memberships.exclude(pk=membership.pk).filter(is_primary=True).update(is_primary=False)
|