first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
Reference in new issue
Block a user