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 '{}'.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 '{}'.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 '{}'.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}"