461 lines
17 KiB
Python
461 lines
17 KiB
Python
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, Broker
|
||
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="اولویت"
|
||
)
|
||
|
||
broker = models.ForeignKey(
|
||
Broker,
|
||
on_delete=models.SET_NULL,
|
||
verbose_name="کارگزار",
|
||
blank=True,
|
||
null=True,
|
||
related_name='process_instances'
|
||
)
|
||
|
||
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 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()
|
||
|
||
# Auto-set broker if not already set
|
||
if not self.broker:
|
||
if self.well and hasattr(self.well, 'broker') and self.well.broker:
|
||
self.broker = self.well.broker
|
||
elif self.requester and hasattr(self.requester, 'profile') and self.requester.profile and hasattr(self.requester.profile, 'broker') and self.requester.profile.broker:
|
||
self.broker = self.requester.profile.broker
|
||
|
||
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-label-{}">{}</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}"
|