shafafiyat/processes/models.py

452 lines
17 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 django.db import models
from django.contrib.auth import get_user_model
from common.models import NameSlugModel, SluggedModel
from simple_history.models import HistoricalRecords
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from accounts.models import Role
from _helpers.utils import generate_unique_slug
import random
User = get_user_model()
class Process(NameSlugModel):
"""مدل فرآیند اصلی"""
description = models.TextField(verbose_name="توضیحات", blank=True)
is_active = models.BooleanField(default=True, verbose_name="فعال")
created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
history = HistoricalRecords()
class Meta:
verbose_name = "فرآیند"
verbose_name_plural = "فرآیندها"
ordering = ['-created']
def __str__(self):
return self.name
class ProcessStep(NameSlugModel):
"""مدل مراحل فرآیند"""
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
order = models.PositiveIntegerField(verbose_name="ترتیب")
description = models.TextField(verbose_name="توضیحات", blank=True)
is_required = models.BooleanField(default=True, verbose_name="اجباری")
estimated_duration = models.PositiveIntegerField(verbose_name="مدت زمان تخمینی (روز)", null=True, blank=True)
# فیلدهای جدید برای کنترل وابستگی‌ها
blocks_previous = models.BooleanField(
default=False,
verbose_name="مسدود کننده مراحل قبلی",
help_text="اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می‌شوند"
)
can_go_back = models.BooleanField(
default=True,
verbose_name="قابل بازگشت",
help_text="آیا می‌توان به مراحل قبلی بازگشت"
)
history = HistoricalRecords()
# Note: approver requirements are defined via StepApproverRequirement through model
# See StepApproverRequirement below
class Meta:
verbose_name = "مرحله فرآیند"
verbose_name_plural = "مراحل فرآیند"
ordering = ['process', 'order']
unique_together = ['process', 'order']
def __str__(self):
return f"{self.process.name} - {self.name}"
def get_dependencies(self):
"""دریافت مراحل وابسته"""
return StepDependency.objects.filter(dependent_step=self).values_list('dependency_step', flat=True)
def get_dependents(self):
"""دریافت مراحلی که به این مرحله وابسته هستند"""
return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
class StepDependency(models.Model):
"""مدل وابستگی بین مراحل"""
dependent_step = models.ForeignKey(
ProcessStep,
on_delete=models.CASCADE,
related_name='dependencies',
verbose_name="مرحله وابسته"
)
dependency_step = models.ForeignKey(
ProcessStep,
on_delete=models.CASCADE,
related_name='dependents',
verbose_name="مرحله مورد نیاز"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
class Meta:
verbose_name = "وابستگی مرحله"
verbose_name_plural = "وابستگی‌های مراحل"
unique_together = ['dependent_step', 'dependency_step']
ordering = ['dependent_step__order', 'dependency_step__order']
def __str__(self):
return f"{self.dependent_step}{self.dependency_step}"
def clean(self):
"""اعتبارسنجی مدل"""
if self.dependent_step == self.dependency_step:
raise ValidationError("مرحله نمی‌تواند به خودش وابسته باشد")
if self.dependent_step.process != self.dependency_step.process:
raise ValidationError("مراحل باید از یک فرآیند باشند")
if self.dependent_step.order <= self.dependency_step.order:
raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
class ProcessInstance(SluggedModel):
code = models.CharField(
max_length=5,
unique=True,
verbose_name="کد درخواست",
help_text="کد ۵ رقمی یکتا برای هر درخواست"
)
"""مدل نمونه فرآیند (برای هر درخواست)"""
PRIORITY_CHOICES = [
('low', 'کم'),
('medium', 'متوسط'),
('high', 'زیاد'),
('urgent', 'فوری'),
]
STATUS_CHOICES = [
('pending', 'در انتظار'),
('in_progress', 'در حال انجام'),
('completed', 'تکمیل شده'),
('cancelled', 'لغو شده'),
('rejected', 'رد شده'),
]
description = models.TextField(
verbose_name="توضیحات درخواست",
blank=True,
null=True
)
process = models.ForeignKey(
Process,
on_delete=models.CASCADE,
related_name='instances',
verbose_name="فرآیند",
null=True,
blank=True
)
well = models.ForeignKey(
'wells.Well',
on_delete=models.CASCADE,
related_name='process_instances',
verbose_name="چاه",
)
representative = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name='representative_instances',
verbose_name="نماینده چاه",
null=True,
)
requester = models.ForeignKey(
User,
on_delete=models.SET_NULL,
verbose_name="درخواست کننده",
null=True,
blank=True
)
current_step = models.ForeignKey(
ProcessStep,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="مرحله فعلی",
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending',
verbose_name="وضعیت"
)
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='medium',
verbose_name="اولویت"
)
completed_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="تاریخ تکمیل"
)
class Meta:
verbose_name = "درخواست"
verbose_name_plural = "درخواست‌ها"
ordering = ['-created']
def __str__(self):
if self.well:
return f"{self.process.name} - {self.well.water_subscription_number}"
return f"{self.process.name} - {self.requester.get_full_name()}"
def clean(self):
"""اعتبارسنجی مدل"""
if self.well and self.representative and self.well.representative != self.representative:
raise ValidationError("نماینده درخواست باید همان نماینده ثبت شده در چاه باشد")
if self.well and self.representative and self.requester == self.representative:
raise ValidationError("درخواست کننده نمی‌تواند نماینده چاه باشد")
def save(self, *args, **kwargs):
# Generate unique 5-digit numeric code if missing
if not self.code:
# Try a few times to avoid rare collisions
for _ in range(10):
candidate = f"{random.randint(10000, 99999)}"
if not ProcessInstance.objects.filter(code=candidate).exists():
self.code = candidate
break
# As a fallback if collision persists (very unlikely)
if not self.code:
self.code = f"{random.randint(10000, 99999)}"
if not self.slug:
slug_text = f"{self.process.name}-{self.well.water_subscription_number if self.well else 'unknown'}-{timezone.now().strftime('%Y%m%d')}"
self.slug = generate_unique_slug(slug_text)
if self.status == 'completed' and not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
status_colors = {
'pending': 'info',
'in_progress': 'primary',
'completed': 'success',
'rejected': 'danger',
'cancelled': 'warning',
}
color = status_colors.get(self.status, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
def get_priority_display_with_color(self):
"""نمایش اولویت با رنگ"""
priority_colors = {
'low': 'success',
'medium': 'info',
'high': 'warning',
'urgent': 'danger',
}
color = priority_colors.get(self.priority, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_priority_display())
def can_edit(self, user):
"""بررسی امکان ویرایش درخواست"""
if self.status == 'pending' and self.requester == user:
return True
if self.representative == user and self.status in ['pending']:
return True
return False
def get_available_steps(self):
"""دریافت مراحل قابل دسترس"""
available_steps = []
for step in self.process.steps.all():
if self.can_access_step(step):
available_steps.append(step)
return available_steps
def can_access_step(self, step):
"""بررسی امکان دسترسی به مرحله"""
dependencies = step.get_dependencies()
for dependency_id in dependencies:
step_instance = self.step_instances.filter(step_id=dependency_id).first()
if not step_instance or step_instance.status != 'completed':
return False
return True
def can_edit_step(self, step):
"""بررسی امکان ویرایش مرحله"""
if step.blocks_previous:
later_steps = self.step_instances.filter(
step__order__gt=step.order,
status='completed'
)
if later_steps.exists():
return False
return True
class StepInstance(models.Model):
"""مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند")
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, verbose_name="مرحله")
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="واگذار شده به")
status = models.CharField(
max_length=20,
choices=[
('pending', 'در انتظار'),
('in_progress', 'در حال انجام'),
('completed', 'تکمیل شده'),
('skipped', 'رد شده'),
('blocked', 'مسدود شده'),
('rejected', 'رد شده و نیاز به اصلاح'),
],
default='pending',
verbose_name="وضعیت"
)
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
history = HistoricalRecords()
class Meta:
verbose_name = "نمونه مرحله"
verbose_name_plural = "نمونه‌های مرحله"
ordering = ['process_instance', 'step__order']
def __str__(self):
return f"{self.process_instance} - {self.step.name}"
def save(self, *args, **kwargs):
"""ذخیره با اعتبارسنجی"""
if self.status == 'in_progress' or self.status == 'completed':
if not self.process_instance.can_access_step(self.step):
raise ValidationError("مراحل وابسته تکمیل نشده‌اند")
if self.status == 'completed' and not self.process_instance.can_edit_step(self.step):
raise ValidationError("این مرحله قابل ویرایش نیست")
super().save(*args, **kwargs)
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
status_colors = {
'pending': 'secondary',
'in_progress': 'primary',
'completed': 'success',
'skipped': 'warning',
'blocked': 'danger',
'rejected': 'danger',
}
color = status_colors.get(self.status, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
def get_rejection_count(self):
"""دریافت تعداد رد شدن‌ها"""
return self.rejections.count()
def get_latest_rejection(self):
"""دریافت آخرین رد شدن"""
return self.rejections.order_by('-created_at').first()
# -------- Multi-role approval helpers --------
def required_roles(self):
return [req.role for req in self.step.approver_requirements.select_related('role').all()]
def approvals_by_role(self):
decisions = {}
for a in self.approvals.select_related('role').order_by('created_at'):
decisions[a.role_id] = a.decision
return decisions
def is_fully_approved(self) -> bool:
req_roles = self.required_roles()
if not req_roles:
return True
role_to_decision = self.approvals_by_role()
for r in req_roles:
if role_to_decision.get(r.id) != 'approved':
return False
return True
class StepRejection(models.Model):
"""مدل رد شدن مرحله"""
step_instance = models.ForeignKey(
StepInstance,
on_delete=models.CASCADE,
related_name='rejections',
verbose_name="نمونه مرحله"
)
rejected_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="رد کننده",
related_name='step_rejections'
)
reason = models.TextField(verbose_name="دلیل رد شدن", help_text="توضیح کامل دلیل رد شدن")
instructions = models.TextField(
verbose_name="دستورالعمل‌های اصلاح",
help_text="دستورالعمل‌هایی برای اصلاح مرحله",
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
history = HistoricalRecords()
class Meta:
verbose_name = "رد شدن مرحله"
verbose_name_plural = "رد شدن‌های مراحل"
ordering = ['-created_at']
def __str__(self):
return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()}"
def save(self, *args, **kwargs):
"""ذخیره با تغییر وضعیت مرحله"""
self.step_instance.status = 'rejected'
self.step_instance.save()
super().save(*args, **kwargs)
class StepApproverRequirement(models.Model):
"""Required approver roles for a step."""
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش تاییدکننده")
required_count = models.PositiveIntegerField(default=1, verbose_name="تعداد موردنیاز")
class Meta:
unique_together = ('step', 'role')
verbose_name = "نیازمندی تایید نقش"
verbose_name_plural = "نیازمندی‌های تایید نقش"
def __str__(self):
return f"{self.step}{self.role} (x{self.required_count})"
class StepApproval(models.Model):
"""Approvals per role for a concrete step instance."""
step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله")
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش")
approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="تاییدکننده")
decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه')
reason = models.TextField(blank=True, verbose_name='علت (برای رد)')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
class Meta:
unique_together = ('step_instance', 'role')
verbose_name = 'تایید مرحله'
verbose_name_plural = 'تاییدهای مرحله'
def __str__(self):
return f"{self.step_instance} - {self.role} - {self.decision}"