Add qoute step.
This commit is contained in:
parent
b71ea45681
commit
6ff4740d04
30 changed files with 3362 additions and 376 deletions
|
@ -46,21 +46,95 @@ class StepDependencyAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(ProcessInstance)
|
||||
class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||
list_display = ['name', 'process', 'requester', 'current_step', 'status', 'started_at', 'progress_display']
|
||||
list_filter = ['process', 'status', 'started_at']
|
||||
search_fields = ['name', 'process__name', 'requester__username', 'requester__first_name']
|
||||
readonly_fields = ['deleted_at', 'started_at', 'completed_at']
|
||||
ordering = ['-started_at']
|
||||
verbose_name = "درخواست"
|
||||
verbose_name_plural = "درخواستها"
|
||||
list_display = [
|
||||
'code',
|
||||
'slug',
|
||||
'well_display',
|
||||
'representative',
|
||||
'requester',
|
||||
'process',
|
||||
'status_display',
|
||||
'priority_display',
|
||||
'created',
|
||||
'progress_display'
|
||||
]
|
||||
list_filter = [
|
||||
'process',
|
||||
'status',
|
||||
'priority',
|
||||
'created',
|
||||
'well__representative'
|
||||
]
|
||||
search_fields = [
|
||||
'code',
|
||||
'slug',
|
||||
'process__name',
|
||||
'requester__username',
|
||||
'requester__first_name',
|
||||
'well__water_subscription_number',
|
||||
'representative__username'
|
||||
]
|
||||
readonly_fields = [
|
||||
'deleted_at',
|
||||
'created',
|
||||
'updated',
|
||||
'completed_at'
|
||||
]
|
||||
autocomplete_fields = [
|
||||
'well',
|
||||
'representative',
|
||||
'requester',
|
||||
'process',
|
||||
'current_step'
|
||||
]
|
||||
ordering = ['-created']
|
||||
|
||||
fieldsets = (
|
||||
('اطلاعات اصلی', {
|
||||
'fields': ('code', 'slug', 'description', 'process')
|
||||
}),
|
||||
('اطلاعات چاه', {
|
||||
'fields': ('well', 'representative')
|
||||
}),
|
||||
('اطلاعات درخواست', {
|
||||
'fields': ('requester', 'priority')
|
||||
}),
|
||||
('وضعیت و پیشرفت', {
|
||||
'fields': ('status', 'current_step')
|
||||
}),
|
||||
('تاریخها', {
|
||||
'fields': ('created', 'updated', 'completed_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def well_display(self, obj):
|
||||
if obj.well:
|
||||
return f"{obj.well.water_subscription_number}"
|
||||
return "-"
|
||||
well_display.short_description = "چاه"
|
||||
|
||||
def status_display(self, obj):
|
||||
return mark_safe(obj.get_status_display_with_color())
|
||||
status_display.short_description = "وضعیت"
|
||||
|
||||
def priority_display(self, obj):
|
||||
return mark_safe(obj.get_priority_display_with_color())
|
||||
priority_display.short_description = "اولویت"
|
||||
|
||||
def progress_display(self, obj):
|
||||
total_steps = obj.process.steps.count()
|
||||
completed_steps = obj.step_instances.filter(status='completed').count()
|
||||
percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
|
||||
percentage_int = int(percentage)
|
||||
return format_html(
|
||||
'<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
|
||||
percentage_int, completed_steps, total_steps, percentage_int
|
||||
)
|
||||
if obj.process:
|
||||
total_steps = obj.process.steps.count()
|
||||
completed_steps = obj.step_instances.filter(status='completed').count()
|
||||
percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
|
||||
percentage_int = int(percentage)
|
||||
return format_html(
|
||||
'<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
|
||||
percentage_int, completed_steps, total_steps, percentage_int
|
||||
)
|
||||
return "-"
|
||||
progress_display.short_description = "پیشرفت"
|
||||
|
||||
@admin.register(StepInstance)
|
||||
|
|
|
@ -4,9 +4,16 @@ from .models import ProcessInstance, StepInstance
|
|||
class ProcessInstanceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ProcessInstance
|
||||
fields = ['name']
|
||||
fields = ['description', 'process', 'well', 'representative', 'requester', 'priority', 'status', 'current_step']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'})
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'process': forms.Select(attrs={'class': 'form-control'}),
|
||||
'well': forms.Select(attrs={'class': 'form-control'}),
|
||||
'representative': forms.Select(attrs={'class': 'form-control'}),
|
||||
'requester': forms.Select(attrs={'class': 'form-control'}),
|
||||
'priority': forms.Select(attrs={'class': 'form-control'}),
|
||||
'status': forms.Select(attrs={'class': 'form-control'}),
|
||||
'current_step': forms.Select(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class StepInstanceForm(forms.ModelForm):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
|
||||
# Generated by Django 5.2.4 on 2025-08-14 09:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
|
@ -11,6 +11,7 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wells', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
|
@ -130,18 +131,21 @@ class Migration(migrations.Migration):
|
|||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
|
||||
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||
('code', models.CharField(help_text='کد ۵ رقمی یکتا برای هر درخواست', max_length=5, unique=True, verbose_name='کد درخواست')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='توضیحات درخواست')),
|
||||
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
|
||||
('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
|
||||
('priority', models.CharField(choices=[('low', 'کم'), ('medium', 'متوسط'), ('high', 'زیاد'), ('urgent', 'فوری')], default='medium', max_length=20, verbose_name='اولویت')),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
|
||||
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
|
||||
('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
|
||||
('process', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
|
||||
('representative', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='representative_instances', to=settings.AUTH_USER_MODEL, verbose_name='نماینده چاه')),
|
||||
('requester', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
|
||||
('well', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='process_instances', to='wells.well', verbose_name='چاه')),
|
||||
('current_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='processes.processstep', verbose_name='مرحله فعلی')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'نمونه فرآیند',
|
||||
'verbose_name_plural': 'نمونه\u200cهای فرآیند',
|
||||
'ordering': ['-started_at'],
|
||||
'verbose_name': 'درخواست',
|
||||
'verbose_name_plural': 'درخواست\u200cها',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
@ -169,37 +173,6 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalProcessInstance',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
|
||||
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
|
||||
('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('requester', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
|
||||
('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
|
||||
('current_step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله فعلی')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical نمونه فرآیند',
|
||||
'verbose_name_plural': 'historical نمونه\u200cهای فرآیند',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StepInstance',
|
||||
fields=[
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from common.models import NameSlugModel
|
||||
from common.models import NameSlugModel, SluggedModel
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from _helpers.utils import generate_unique_slug
|
||||
import random
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -21,6 +24,7 @@ class Process(NameSlugModel):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ProcessStep(NameSlugModel):
|
||||
"""مدل مراحل فرآیند"""
|
||||
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
|
||||
|
@ -95,35 +99,169 @@ class StepDependency(models.Model):
|
|||
if self.dependent_step.order <= self.dependency_step.order:
|
||||
raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
|
||||
|
||||
class ProcessInstance(NameSlugModel):
|
||||
|
||||
class ProcessInstance(SluggedModel):
|
||||
code = models.CharField(
|
||||
max_length=5,
|
||||
unique=True,
|
||||
verbose_name="کد درخواست",
|
||||
help_text="کد ۵ رقمی یکتا برای هر درخواست"
|
||||
)
|
||||
"""مدل نمونه فرآیند (برای هر درخواست)"""
|
||||
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='instances', verbose_name="فرآیند")
|
||||
requester = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="درخواست کننده")
|
||||
current_step = models.ForeignKey('ProcessStep', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="مرحله فعلی")
|
||||
|
||||
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=[
|
||||
('pending', 'در انتظار'),
|
||||
('in_progress', 'در حال انجام'),
|
||||
('completed', 'تکمیل شده'),
|
||||
('cancelled', 'لغو شده'),
|
||||
('rejected', 'رد شده'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='pending',
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
|
||||
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
|
||||
history = HistoricalRecords()
|
||||
|
||||
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 = ['-started_at']
|
||||
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 = []
|
||||
|
@ -134,7 +272,6 @@ class ProcessInstance(NameSlugModel):
|
|||
|
||||
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()
|
||||
|
@ -144,7 +281,6 @@ class ProcessInstance(NameSlugModel):
|
|||
|
||||
def can_edit_step(self, step):
|
||||
"""بررسی امکان ویرایش مرحله"""
|
||||
# اگر مرحله مسدود کننده باشد و مراحل بعدی تکمیل شده باشند
|
||||
if step.blocks_previous:
|
||||
later_steps = self.step_instances.filter(
|
||||
step__order__gt=step.order,
|
||||
|
@ -187,12 +323,10 @@ class StepInstance(models.Model):
|
|||
|
||||
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("این مرحله قابل ویرایش نیست")
|
||||
|
||||
|
@ -252,7 +386,6 @@ class StepRejection(models.Model):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""ذخیره با تغییر وضعیت مرحله"""
|
||||
# تغییر وضعیت مرحله به رد شده
|
||||
self.step_instance.status = 'rejected'
|
||||
self.step_instance.save()
|
||||
super().save(*args, **kwargs)
|
||||
|
|
54
processes/templates/processes/includes/stepper_header.html
Normal file
54
processes/templates/processes/includes/stepper_header.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="bs-stepper-header">
|
||||
{% for item in steps_context %}
|
||||
{% with step=item.step status=item.status can_access=item.can_access is_selected=item.is_selected is_todo=item.is_todo %}
|
||||
<div class="step
|
||||
{% if not can_access %}disabled
|
||||
{% elif status == 'completed' %}completed
|
||||
{% elif is_todo %}active
|
||||
{% endif %}
|
||||
{% if is_selected %} selected{% endif %}"
|
||||
data-target="#step-{{ step.id }}">
|
||||
|
||||
{% if can_access %}
|
||||
<a href="{% url 'processes:step_detail' instance.id step.id %}"
|
||||
class="step-trigger text-decoration-none p-0"
|
||||
aria-selected="{% if is_selected %}true{% else %}false{% endif %}">
|
||||
{% else %}
|
||||
<span class="step-trigger">
|
||||
{% endif %}
|
||||
|
||||
<span class="bs-stepper-circle">{{ forloop.counter }}</span>
|
||||
<span class="bs-stepper-label mt-1">
|
||||
<span class="bs-stepper-title">{{ step.name }}</span>
|
||||
<span class="bs-stepper-subtitle">{{ step.description|default:' ' }}</span>
|
||||
</span>
|
||||
|
||||
{% if can_access %}
|
||||
</a>
|
||||
{% else %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% if not forloop.last %}<div class="line"></div>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step.disabled .step-trigger { opacity: 0.5; cursor: not-allowed; }
|
||||
.step.disabled .bs-stepper-circle { background-color: #6c757d; }
|
||||
|
||||
/* تکمیل شده */
|
||||
.step.completed .bs-stepper-circle { background-color: #28a745 !important; color: #fff !important; border-color: #28a745 !important; }
|
||||
.step.completed .bs-stepper-title { color: #28a745 !important; }
|
||||
|
||||
/* مرحلهای که باید انجام شود (فعلی سیستم) */
|
||||
.step.active .bs-stepper-circle { background-color: #696cff; color: #fff; }
|
||||
.step.active .bs-stepper-title { color: #696cff; }
|
||||
|
||||
/* مرحله انتخابشده (نمایش فعلی صفحه) */
|
||||
.step.selected { outline: 1px solid #696cff; border-radius: 8px; }
|
||||
.step.selected .bs-stepper-circle { box-shadow: 0 0 0 3px rgba(13,202,240,.25); }
|
||||
</style>
|
774
processes/templates/processes/request_list.html
Normal file
774
processes/templates/processes/request_list.html
Normal file
|
@ -0,0 +1,774 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'sidebars/admin.html' %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include 'navbars/admin.html' %}
|
||||
{% endblock navbar %}
|
||||
|
||||
|
||||
{% block title %}درخواستها{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-bs5/datatables.bootstrap5.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.css' %}">
|
||||
<!-- Persian Date Picker CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/persian-datepicker@latest/dist/css/persian-datepicker.min.css">
|
||||
|
||||
{% endblock style %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include '_toasts.html' %}
|
||||
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">درخواستها</h4>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
|
||||
<i class="bx bx-plus"></i>
|
||||
درخواست جدید
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table id="requestsTable" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شناسه</th>
|
||||
<th>فرآیند</th>
|
||||
<th>شماره اشتراک آب</th>
|
||||
<th>نماینده</th>
|
||||
<th>درخواستکننده</th>
|
||||
<th>اولویت</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تاریخ ایجاد</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inst in instances %}
|
||||
<tr>
|
||||
<td>{{ inst.code }}</td>
|
||||
<td>{{ inst.process.name }}</td>
|
||||
<td>{{ inst.well.water_subscription_number }}</td>
|
||||
<td>{% if inst.representative %}{{ inst.representative.get_full_name }}{% else %}-{% endif %}</td>
|
||||
<td>{% if inst.requester %}{{ inst.requester.get_full_name }}{% else %}-{% endif %}</td>
|
||||
<td>{{ inst.get_priority_display }}</td>
|
||||
<td>{{ inst.get_status_display }}</td>
|
||||
<td>{{ inst.jcreated }}</td>
|
||||
<td>
|
||||
<div class="d-inline-block">
|
||||
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
|
||||
<i class="icon-base bx bx-dots-vertical-rounded"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end m-0">
|
||||
<li>
|
||||
<a href="{% url 'processes:instance_steps' inst.id %}" class="dropdown-item">
|
||||
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
||||
</a>
|
||||
</li>
|
||||
<div class="dropdown-divider"></div>
|
||||
<li>
|
||||
<a href="#" class="dropdown-item text-danger" data-instance-id="{{ inst.id }}" data-instance-code="{{ inst.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
|
||||
<i class="bx bx-trash me-1"></i>حذف
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">موردی ثبت نشده است</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="requestModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">درخواست جدید</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="requestForm">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label">فرآیند</label>
|
||||
<select class="form-select" name="process" id="req_process" required>
|
||||
{% for process in processes %}
|
||||
<option value="{{ process.id }}">{{ process.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<hr class="mt-3 border border-dashed">
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label">شماره اشتراک آب</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="req_water_sub" placeholder="مثال: 12345" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="btnLookupWell">
|
||||
بررسی/افزودن چاه
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text" id="wellStatus"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Well form fields (from WellForm) -->
|
||||
<div id="wellFormBlock" class="col-sm-12" style="display:none;">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_electricity_subscription_number">{{ well_form.electricity_subscription_number.label }}</label>
|
||||
{{ well_form.electricity_subscription_number }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_water_meter_manufacturer">{{ well_form.water_meter_manufacturer.label }}</label>
|
||||
<div class="input-group">
|
||||
<select name="water_meter_manufacturer" class="form-select" id="id_water_meter_manufacturer">
|
||||
<option value="" selected="">انتخاب شرکت سازنده</option>
|
||||
{% for manufacturer in manufacturers %}
|
||||
<option value="{{ manufacturer.id }}">{{ manufacturer.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" class="form-control" id="id_new_manufacturer" name="new_manufacturer" placeholder="شرکت سازنده جدید" style="display:none;">
|
||||
<button class="btn btn-outline-primary" type="button" id="btnToggleManufacturer">
|
||||
<i class="bx bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_water_meter_serial_number">{{ well_form.water_meter_serial_number.label }}</label>
|
||||
{{ well_form.water_meter_serial_number }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_water_meter_old_serial_number">{{ well_form.water_meter_old_serial_number.label }}</label>
|
||||
{{ well_form.water_meter_old_serial_number }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_utm_x">{{ well_form.utm_x.label }}</label>
|
||||
{{ well_form.utm_x }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_utm_y">{{ well_form.utm_y.label }}</label>
|
||||
{{ well_form.utm_y }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_utm_zone">{{ well_form.utm_zone.label }}</label>
|
||||
{{ well_form.utm_zone }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_utm_hemisphere">{{ well_form.utm_hemisphere.label }}</label>
|
||||
{{ well_form.utm_hemisphere }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_well_power">{{ well_form.well_power.label }}</label>
|
||||
{{ well_form.well_power }}
|
||||
</div>
|
||||
<div class="col-sm-6"></div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_reference_letter_number">{{ well_form.reference_letter_number.label }}</label>
|
||||
{{ well_form.reference_letter_number }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_reference_letter_date">{{ well_form.reference_letter_date.label }}</label>
|
||||
<input type="text" class="form-control" id="id_reference_letter_date" name="reference_letter_date" placeholder="انتخاب تاریخ" readonly>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label" for="id_representative_letter_file">{{ well_form.representative_letter_file.label }}</label>
|
||||
{{ well_form.representative_letter_file }}
|
||||
<!-- نمایش فایل موجود -->
|
||||
<div id="current-file-display" style="display: none; margin-top: 10px;">
|
||||
<div class="alert alert-info d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bx bx-file me-2"></i>
|
||||
<span id="current-file-name" class="text-truncate" style="max-width: 200px;" title=""></span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeCurrentFile()">
|
||||
<i class="bx bx-trash me-1"></i>حذف
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="remove-file" name="remove_file" value="false">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-3 border border-dashed">
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label">کد ملی نماینده</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="rep_national_code" placeholder="مثال: 0012345678">
|
||||
<button class="btn btn-outline-secondary" type="button" id="btnLookupRep">
|
||||
بررسی/افزودن نماینده
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text" id="repStatus"></div>
|
||||
</div>
|
||||
|
||||
<div id="repNewFields" class="col-sm-12" style="display:none;">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_first_name">{{ customer_form.first_name.label }}</label>
|
||||
{{ customer_form.first_name }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_last_name">{{ customer_form.last_name.label }}</label>
|
||||
{{ customer_form.last_name }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_phone_number_1">{{ customer_form.phone_number_1.label }}</label>
|
||||
{{ customer_form.phone_number_1 }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_phone_number_2">{{ customer_form.phone_number_2.label }}</label>
|
||||
{{ customer_form.phone_number_2 }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_national_code">{{ customer_form.national_code.label }}</label>
|
||||
{{ customer_form.national_code }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_card_number">{{ customer_form.card_number.label }}</label>
|
||||
{{ customer_form.card_number }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="id_account_number">{{ customer_form.account_number.label }}</label>
|
||||
{{ customer_form.account_number }}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label" for="id_address">{{ customer_form.address.label }}</label>
|
||||
{{ customer_form.address }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-3 border border-dashed">
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label">توضیحات</label>
|
||||
<textarea class="form-control" rows="3" id="req_description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">بستن</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSaveRequest" disabled>ذخیره</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteConfirmModalLabel">تایید حذف</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="deleteConfirmText">آیا از حذف این درخواست اطمینان دارید؟</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<!-- DataTables JS -->
|
||||
<script src="{% static 'assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js' %}"></script>
|
||||
<!-- Persian DataTable defaults -->
|
||||
<script src="{% static 'assets/js/persian-datatable.js' %}"></script>
|
||||
|
||||
<!-- Persian Date Picker JS -->
|
||||
<script src="https://unpkg.com/persian-date@latest/dist/persian-date.min.js"></script>
|
||||
<script src="https://unpkg.com/persian-datepicker@latest/dist/js/persian-datepicker.min.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
// Function to initialize Persian Date Picker
|
||||
function initPersianDatePicker() {
|
||||
if ($.fn.persianDatepicker && $('#id_reference_letter_date').length) {
|
||||
try {
|
||||
$('#id_reference_letter_date').persianDatepicker({
|
||||
format: 'YYYY/MM/DD',
|
||||
initialValue: false,
|
||||
autoClose: true,
|
||||
persianDigit: false,
|
||||
observer: true,
|
||||
calendar: {
|
||||
persian: {
|
||||
locale: 'fa',
|
||||
leapYearMode: 'astronomical'
|
||||
}
|
||||
},
|
||||
onSelect: function(unix) {
|
||||
// تبدیل تاریخ شمسی به میلادی برای ارسال به سرور
|
||||
const gregorianDate = new Date(unix);
|
||||
const year = gregorianDate.getFullYear();
|
||||
const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(gregorianDate.getDate()).padStart(2, '0');
|
||||
const gregorianDateString = `${year}-${month}-${day}`;
|
||||
|
||||
// نمایش تاریخ شمسی در فیلد
|
||||
if (window.persianDate) {
|
||||
const persianDate = new window.persianDate(unix);
|
||||
const persianDateString = persianDate.format('YYYY/MM/DD');
|
||||
$('#id_reference_letter_date').val(persianDateString);
|
||||
} else {
|
||||
// اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
|
||||
$('#id_reference_letter_date').val(gregorianDateString);
|
||||
}
|
||||
|
||||
// ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
|
||||
$('#id_reference_letter_date').attr('data-gregorian', gregorianDateString);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error initializing Persian Date Picker:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$(function() {
|
||||
// if ($.fn.DataTable) {
|
||||
// try {
|
||||
// $('#requestsTable').DataTable({
|
||||
// pageLength: 10,
|
||||
// order: [[0, 'desc']]
|
||||
// });
|
||||
// } catch (e) {
|
||||
// console.error('DataTable init failed', e);
|
||||
// }
|
||||
// } else {
|
||||
// console.warn('DataTables library not loaded');
|
||||
// }
|
||||
|
||||
let currentWellId = null;
|
||||
let currentRepId = null;
|
||||
let wellChecked = false;
|
||||
let repChecked = false;
|
||||
|
||||
function setStatus(el, text, type) {
|
||||
$(el).text(text).removeClass('text-danger text-success text-muted').addClass(type ? 'text-' + type : 'text-muted');
|
||||
}
|
||||
|
||||
function checkSaveButton() {
|
||||
const canSave = wellChecked && repChecked;
|
||||
$('#btnSaveRequest').prop('disabled', !canSave);
|
||||
}
|
||||
|
||||
// Inline error helpers
|
||||
function clearInlineErrors() {
|
||||
$('#requestModal .is-invalid').removeClass('is-invalid');
|
||||
$('#requestModal .invalid-feedback.inline-error').remove();
|
||||
}
|
||||
|
||||
function applyErrorTo(selector, message) {
|
||||
const $el = $(selector);
|
||||
if (!$el.length) return false;
|
||||
$el.addClass('is-invalid');
|
||||
const $feedback = $('<div class="invalid-feedback inline-error"></div>').text(message);
|
||||
const $grp = $el.closest('.input-group');
|
||||
if ($grp.length) {
|
||||
$feedback.insertAfter($grp);
|
||||
} else {
|
||||
$feedback.insertAfter($el);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mapWellFieldToSelector(field) {
|
||||
switch (field) {
|
||||
case 'water_subscription_number': return '#req_water_sub';
|
||||
case 'electricity_subscription_number': return '#id_electricity_subscription_number';
|
||||
case 'water_meter_serial_number': return '#id_water_meter_serial_number';
|
||||
case 'water_meter_old_serial_number': return '#id_water_meter_old_serial_number';
|
||||
case 'water_meter_manufacturer': return '#id_water_meter_manufacturer';
|
||||
case 'new_manufacturer': return '#id_new_manufacturer';
|
||||
case 'utm_x': return '#id_utm_x';
|
||||
case 'utm_y': return '#id_utm_y';
|
||||
case 'utm_zone': return '#id_utm_zone';
|
||||
case 'utm_hemisphere': return '#id_utm_hemisphere';
|
||||
case 'well_power': return '#id_well_power';
|
||||
case 'reference_letter_number': return '#id_reference_letter_number';
|
||||
case 'reference_letter_date': return '#id_reference_letter_date';
|
||||
case 'representative_letter_file': return '#id_representative_letter_file';
|
||||
case 'representative': return '#rep_national_code';
|
||||
default: return '#id_' + field;
|
||||
}
|
||||
}
|
||||
|
||||
function mapCustomerFieldToSelector(field) {
|
||||
switch (field) {
|
||||
case 'national_code': return $('#id_national_code').length ? '#id_national_code' : '#rep_national_code';
|
||||
case 'first_name': return '#id_first_name';
|
||||
case 'last_name': return '#id_last_name';
|
||||
case 'phone_number_1': return '#id_phone_number_1';
|
||||
case 'phone_number_2': return '#id_phone_number_2';
|
||||
case 'card_number': return '#id_card_number';
|
||||
case 'account_number': return '#id_account_number';
|
||||
case 'address': return '#id_address';
|
||||
default: return '#id_' + field;
|
||||
}
|
||||
}
|
||||
|
||||
function showInlineErrors(errors) {
|
||||
if (!errors) return;
|
||||
let nonFieldWell = '';
|
||||
let nonFieldCustomer = '';
|
||||
if (errors.well) {
|
||||
for (const key in errors.well) {
|
||||
const msgs = Array.isArray(errors.well[key]) ? errors.well[key] : [errors.well[key]];
|
||||
if (key === '__all__' || key === 'non_field_errors') { nonFieldWell = msgs.join('، '); continue; }
|
||||
const sel = mapWellFieldToSelector(key);
|
||||
applyErrorTo(sel, msgs[0]);
|
||||
}
|
||||
}
|
||||
if (errors.customer) {
|
||||
for (const key in errors.customer) {
|
||||
const msgs = Array.isArray(errors.customer[key]) ? errors.customer[key] : [errors.customer[key]];
|
||||
if (key === '__all__' || key === 'non_field_errors') { nonFieldCustomer = msgs.join('، '); continue; }
|
||||
const sel = mapCustomerFieldToSelector(key);
|
||||
applyErrorTo(sel, msgs[0]);
|
||||
}
|
||||
}
|
||||
if (nonFieldWell) setStatus('#wellStatus', nonFieldWell, 'danger');
|
||||
if (nonFieldCustomer) setStatus('#repStatus', nonFieldCustomer, 'danger');
|
||||
}
|
||||
|
||||
$('#btnLookupWell').on('click', function() {
|
||||
const sub = $('#req_water_sub').val().trim();
|
||||
if (!sub) { setStatus('#wellStatus', 'لطفا شماره اشتراک آب را وارد کنید', 'danger'); return; }
|
||||
setStatus('#wellStatus', 'در حال بررسی...', 'muted');
|
||||
wellChecked = true;
|
||||
checkSaveButton();
|
||||
$.get('{% url "processes:lookup_well_by_subscription" %}', { water_subscription_number: sub })
|
||||
.done(function(resp){
|
||||
if (resp.exists) {
|
||||
currentWellId = resp.well.id;
|
||||
$('#wellFormBlock').show();
|
||||
// Initialize Persian Date Picker after well form is shown
|
||||
setTimeout(initPersianDatePicker, 100);
|
||||
|
||||
// Prefill well form
|
||||
$('#id_electricity_subscription_number').val(resp.well.electricity_subscription_number || '');
|
||||
$('#id_water_meter_serial_number').val(resp.well.water_meter_serial_number || '');
|
||||
$('#id_water_meter_old_serial_number').val(resp.well.water_meter_old_serial_number || '');
|
||||
$('#id_water_meter_manufacturer').val(resp.well.water_meter_manufacturer || '');
|
||||
$('#id_utm_x').val(resp.well.utm_x || '');
|
||||
$('#id_utm_y').val(resp.well.utm_y || '');
|
||||
$('#id_utm_zone').val(resp.well.utm_zone || '');
|
||||
$('#id_utm_hemisphere').val(resp.well.utm_hemisphere || '');
|
||||
$('#id_well_power').val(resp.well.well_power || '');
|
||||
$('#id_reference_letter_number').val(resp.well.reference_letter_number || '');
|
||||
// Prefill date: show Persian in input, keep Gregorian in data attribute
|
||||
if (resp.well.reference_letter_date) {
|
||||
try {
|
||||
if (window.persianDate) {
|
||||
const gregorianDate = new Date(resp.well.reference_letter_date);
|
||||
const persianDateObj = new window.persianDate(gregorianDate);
|
||||
const persianDateString = persianDateObj.format('YYYY/MM/DD');
|
||||
$('#id_reference_letter_date').val(persianDateString);
|
||||
} else {
|
||||
$('#id_reference_letter_date').val(resp.well.reference_letter_date);
|
||||
}
|
||||
$('#id_reference_letter_date').attr('data-gregorian', resp.well.reference_letter_date);
|
||||
} catch (e) {
|
||||
$('#id_reference_letter_date').val(resp.well.reference_letter_date);
|
||||
}
|
||||
} else {
|
||||
$('#id_reference_letter_date').val('');
|
||||
$('#id_reference_letter_date').removeAttr('data-gregorian');
|
||||
}
|
||||
// Existing representative letter file display
|
||||
if (resp.well.representative_letter_file_url) {
|
||||
$('#current-file-display').show();
|
||||
const fileName = resp.well.representative_letter_file_name || 'فایل موجود';
|
||||
$('#current-file-name').text(fileName).attr('title', fileName);
|
||||
$('#id_representative_letter_file').hide();
|
||||
$('#remove-file').val('false');
|
||||
} else {
|
||||
$('#current-file-display').hide();
|
||||
$('#id_representative_letter_file').show();
|
||||
$('#remove-file').val('false');
|
||||
}
|
||||
setStatus('#wellStatus', 'چاه یافت شد', 'success');
|
||||
} else {
|
||||
currentWellId = null;
|
||||
$('#wellFormBlock').show();
|
||||
$('#wellFormBlock').find('input, select').val('');
|
||||
$('#id_reference_letter_date').removeAttr('data-gregorian');
|
||||
// Reset file UI for new well
|
||||
$('#current-file-display').hide();
|
||||
$('#id_representative_letter_file').show().val('');
|
||||
$('#remove-file').val('false');
|
||||
// Initialize Persian Date Picker after well form is shown
|
||||
setTimeout(initPersianDatePicker, 100);
|
||||
setStatus('#wellStatus', 'چاه یافت نشد. با ذخیره، ایجاد خواهد شد.', 'danger');
|
||||
}
|
||||
})
|
||||
.fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
|
||||
});
|
||||
|
||||
$('#btnLookupRep').on('click', function() {
|
||||
const nc = $('#rep_national_code').val().trim();
|
||||
if (!nc) { setStatus('#repStatus', 'لطفا کد ملی نماینده را وارد کنید', 'danger'); return; }
|
||||
setStatus('#repStatus', 'در حال بررسی...', 'muted');
|
||||
repChecked = true;
|
||||
checkSaveButton();
|
||||
$.get('{% url "processes:lookup_representative_by_national_code" %}', { national_code: nc })
|
||||
.done(function(resp){
|
||||
if (resp.exists) {
|
||||
currentRepId = resp.user.id;
|
||||
$('#repNewFields').show();
|
||||
// Prefill customer form fields for editing
|
||||
$('#id_first_name').val(resp.user.first_name || '');
|
||||
$('#id_last_name').val(resp.user.last_name || '');
|
||||
if (resp.user.profile) {
|
||||
$('#id_national_code').val(resp.user.profile.national_code || nc);
|
||||
$('#id_phone_number_1').val(resp.user.profile.phone_number_1 || '');
|
||||
$('#id_phone_number_2').val(resp.user.profile.phone_number_2 || '');
|
||||
$('#id_card_number').val(resp.user.profile.card_number || '');
|
||||
$('#id_account_number').val(resp.user.profile.account_number || '');
|
||||
$('#id_address').val(resp.user.profile.address || '');
|
||||
} else {
|
||||
$('#id_national_code').val(nc);
|
||||
$('#id_phone_number_1').val('');
|
||||
$('#id_phone_number_2').val('');
|
||||
$('#id_card_number').val('');
|
||||
$('#id_account_number').val('');
|
||||
$('#id_address').val('');
|
||||
}
|
||||
setStatus('#repStatus', 'نماینده یافت شد.', 'success');
|
||||
} else {
|
||||
currentRepId = null;
|
||||
$('#repNewFields').show();
|
||||
// Clear form and prefill national code
|
||||
$('#id_first_name').val('');
|
||||
$('#id_last_name').val('');
|
||||
$('#id_national_code').val(nc);
|
||||
$('#id_phone_number_1').val('');
|
||||
$('#id_phone_number_2').val('');
|
||||
$('#id_card_number').val('');
|
||||
$('#id_account_number').val('');
|
||||
$('#id_address').val('');
|
||||
setStatus('#repStatus', 'نماینده یافت نشد. لطفا اطلاعات را تکمیل کنید.', 'danger');
|
||||
}
|
||||
})
|
||||
.fail(function(){ setStatus('#repStatus', 'خطا در بررسی نماینده', 'danger'); });
|
||||
});
|
||||
|
||||
$('#btnSaveRequest').on('click', function(){
|
||||
const formData = new FormData();
|
||||
formData.append('csrfmiddlewaretoken', $('input[name=csrfmiddlewaretoken]').val());
|
||||
formData.append('process', $('#req_process').val());
|
||||
formData.append('description', $('#req_description').val());
|
||||
formData.append('water_subscription_number', $('#req_water_sub').val().trim());
|
||||
if (currentWellId) formData.append('well_id', currentWellId);
|
||||
if (currentRepId) formData.append('representative_id', currentRepId);
|
||||
// Send fields using CustomerForm names if visible
|
||||
const ncField = $('#id_national_code').length ? $('#id_national_code').val() : '';
|
||||
formData.append('national_code', (ncField || $('#rep_national_code').val().trim()));
|
||||
formData.append('first_name', $('#id_first_name').val() || '');
|
||||
formData.append('last_name', $('#id_last_name').val() || '');
|
||||
formData.append('phone_number_1', $('#id_phone_number_1').val() || '');
|
||||
formData.append('phone_number_2', $('#id_phone_number_2').val() || '');
|
||||
formData.append('card_number', $('#id_card_number').val() || '');
|
||||
formData.append('account_number', $('#id_account_number').val() || '');
|
||||
formData.append('address', $('#id_address').val() || '');
|
||||
|
||||
// Include WellForm fields so edits are saved
|
||||
if ($('#wellFormBlock').is(':visible')) {
|
||||
formData.append('electricity_subscription_number', $('#id_electricity_subscription_number').val() || '');
|
||||
formData.append('water_meter_serial_number', $('#id_water_meter_serial_number').val() || '');
|
||||
formData.append('water_meter_old_serial_number', $('#id_water_meter_old_serial_number').val() || '');
|
||||
formData.append('water_meter_manufacturer', $('#id_water_meter_manufacturer').is(':visible') ? ($('#id_water_meter_manufacturer').val() || '') : '');
|
||||
formData.append('new_manufacturer', $('#id_new_manufacturer').is(':visible') ? ($('#id_new_manufacturer').val() || '') : '');
|
||||
formData.append('utm_x', $('#id_utm_x').val() || '');
|
||||
formData.append('utm_y', $('#id_utm_y').val() || '');
|
||||
formData.append('utm_zone', $('#id_utm_zone').val() || '');
|
||||
formData.append('utm_hemisphere', $('#id_utm_hemisphere').val() || '');
|
||||
formData.append('well_power', $('#id_well_power').val() || '');
|
||||
formData.append('reference_letter_number', $('#id_reference_letter_number').val() || '');
|
||||
// Use gregorian date if available, otherwise use the field value
|
||||
const gregorianDate = $('#id_reference_letter_date').attr('data-gregorian');
|
||||
formData.append('reference_letter_date', gregorianDate || $('#id_reference_letter_date').val() || '');
|
||||
// Remove flag
|
||||
formData.append('remove_file', $('#remove-file').val() || 'false');
|
||||
const repFile = document.getElementById('id_representative_letter_file');
|
||||
if (repFile && repFile.files && repFile.files[0]) {
|
||||
formData.append('representative_letter_file', repFile.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const $btn = $(this).prop('disabled', true).text('در حال ذخیره...');
|
||||
$.ajax({
|
||||
url: '{% url "processes:create_request_with_entities" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
}).done(function(resp){
|
||||
if (resp.ok) {
|
||||
showToast('درخواست با موفقیت ثبت شد', 'success');
|
||||
if (resp.redirect) {
|
||||
setTimeout(function(){ window.location.href = resp.redirect; }, 800);
|
||||
} else {
|
||||
setTimeout(function(){ location.reload(); }, 1200);
|
||||
}
|
||||
} else {
|
||||
const msg = buildErrorMessage(resp);
|
||||
showToast(msg, 'danger');
|
||||
}
|
||||
}).fail(function(xhr){
|
||||
let msg = 'خطا در ذخیره';
|
||||
try {
|
||||
const resp = JSON.parse(xhr.responseText);
|
||||
msg = buildErrorMessage(resp) || msg;
|
||||
} catch(e) {}
|
||||
showToast(msg, 'danger');
|
||||
}).always(function(){
|
||||
$btn.prop('disabled', false).text('ذخیره');
|
||||
});
|
||||
});
|
||||
|
||||
function buildErrorMessage(resp){
|
||||
if (!resp) return '';
|
||||
if (resp.error) return resp.error;
|
||||
if (resp.errors) {
|
||||
// Collect form-related errors
|
||||
const parts = [];
|
||||
if (resp.errors.customer) {
|
||||
parts.push('خطای نماینده: ' + flattenErrors(resp.errors.customer));
|
||||
}
|
||||
if (resp.errors.well) {
|
||||
parts.push('خطای چاه: ' + flattenErrors(resp.errors.well));
|
||||
}
|
||||
return parts.join(' | ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function flattenErrors(errorsObj){
|
||||
if (typeof errorsObj === 'string') return errorsObj;
|
||||
try {
|
||||
const parts = [];
|
||||
for (const k in errorsObj){
|
||||
const v = errorsObj[k];
|
||||
if (Array.isArray(v)) parts.push(`${k}: ${v[0]}`);
|
||||
else if (typeof v === 'string') parts.push(`${k}: ${v}`);
|
||||
}
|
||||
return parts.join('، ');
|
||||
} catch(e){
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$('#btnToggleManufacturer').on('click', function() {
|
||||
const $select = $('#id_water_meter_manufacturer');
|
||||
const $input = $('#id_new_manufacturer');
|
||||
const $btn = $(this);
|
||||
|
||||
if ($select.is(':visible')) {
|
||||
$select.hide();
|
||||
$input.show().focus();
|
||||
$btn.html('<i class="bx bx-check"></i>');
|
||||
} else {
|
||||
$input.hide();
|
||||
$select.show();
|
||||
$btn.html('<i class="bx bx-plus"></i>');
|
||||
}
|
||||
});
|
||||
|
||||
$('#requestModal').on('hidden.bs.modal', function(){
|
||||
$('#requestForm')[0].reset();
|
||||
$('#wellFormBlock').hide();
|
||||
$('#repNewFields').hide();
|
||||
$('#id_reference_letter_date').removeAttr('data-gregorian');
|
||||
// Reset file UI
|
||||
$('#current-file-display').hide();
|
||||
$('#id_representative_letter_file').show().val('');
|
||||
$('#remove-file').val('false');
|
||||
setStatus('#wellStatus', '', '');
|
||||
setStatus('#repStatus', '', '');
|
||||
currentWellId = null;
|
||||
currentRepId = null;
|
||||
wellChecked = false;
|
||||
repChecked = false;
|
||||
checkSaveButton();
|
||||
clearInlineErrors(); // Clear inline errors on modal close
|
||||
});
|
||||
|
||||
// Handle selecting a new file: hide existing file display and cancel removal flag
|
||||
$('#id_representative_letter_file').on('change', function() {
|
||||
if (this.files && this.files.length > 0) {
|
||||
$('#current-file-display').hide();
|
||||
$('#remove-file').val('false');
|
||||
}
|
||||
});
|
||||
|
||||
// Expose remove function
|
||||
window.removeCurrentFile = function() {
|
||||
$('#current-file-display').hide();
|
||||
$('#remove-file').val('true');
|
||||
$('#id_representative_letter_file').show().val('');
|
||||
};
|
||||
|
||||
// Delete request function
|
||||
window.deleteRequest = function(instanceId, instanceCode) {
|
||||
// Set modal content
|
||||
document.getElementById('deleteConfirmText').textContent = `آیا از حذف درخواست ${instanceCode} اطمینان دارید؟`;
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
|
||||
modal.show();
|
||||
|
||||
// Handle confirm button click
|
||||
document.getElementById('confirmDeleteBtn').onclick = function() {
|
||||
$.ajax({
|
||||
url: '{% url "processes:delete_request" 0 %}'.replace('0', instanceId),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showToast(response.message, 'success');
|
||||
modal.hide();
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast(response.message, 'danger');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showToast('خطا در ارتباط با سرور', 'danger');
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
97
processes/templates/processes/step_detail.html
Normal file
97
processes/templates/processes/step_detail.html
Normal file
|
@ -0,0 +1,97 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load processes_tags %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'sidebars/admin.html' %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include 'navbars/admin.html' %}
|
||||
{% endblock navbar %}
|
||||
|
||||
{% block title %}{{ step.name }} - درخواست {{ instance.code }}{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_toasts.html' %}
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
||||
<small class="text-muted d-block">
|
||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
||||
</small>
|
||||
</div>
|
||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
||||
</div>
|
||||
|
||||
<div class="bs-stepper wizard-vertical vertical mt-2">
|
||||
{% stepper_header instance step %}
|
||||
|
||||
<div class="bs-stepper-content">
|
||||
<div class="content active dstepper-block">
|
||||
<div class="content-header mb-3">
|
||||
<h6 class="mb-0">{{ step.name }}</h6>
|
||||
<small>{{ step.description|default:' ' }}</small>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h6>وضعیت مرحله:
|
||||
{% if step_instance %}
|
||||
{{ step_instance.get_status_display_with_color|safe }}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">در انتظار</span>
|
||||
{% endif %}
|
||||
</h6>
|
||||
<p class="mb-0">فرم این مرحله بعداً پیادهسازی میشود.</p>
|
||||
|
||||
{% if step_instance and step_instance.notes %}
|
||||
<hr>
|
||||
<strong>یادداشتها:</strong>
|
||||
<p class="mb-0">{{ step_instance.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-between">
|
||||
{% if previous_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}"
|
||||
class="btn btn-label-secondary">
|
||||
<i class="bx bx-chevron-left bx-sm ms-sm-n2"></i>
|
||||
<span class="align-middle d-sm-inline-block d-none">قبلی</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
|
||||
{% if next_step %}
|
||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
||||
class="btn btn-primary">
|
||||
<span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
|
||||
<i class="bx bx-chevron-right bx-sm me-sm-n2"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="button">اتمام</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.js' %}"></script>
|
||||
{% endblock %}
|
0
processes/templatetags/__init__.py
Normal file
0
processes/templatetags/__init__.py
Normal file
48
processes/templatetags/processes_tags.py
Normal file
48
processes/templatetags/processes_tags.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from ..models import ProcessInstance, StepInstance
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='get_item')
|
||||
def get_item(mapping, key):
|
||||
try:
|
||||
return mapping.get(key)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@register.inclusion_tag('processes/includes/stepper_header.html')
|
||||
def stepper_header(instance, current_step=None):
|
||||
"""رندر کردن header مراحل برای instance"""
|
||||
if not isinstance(instance, ProcessInstance):
|
||||
return {}
|
||||
|
||||
steps = list(instance.process.steps.all().order_by('order'))
|
||||
step_instances = instance.step_instances.select_related('step').all()
|
||||
step_id_to_status = {si.step_id: si.status for si in step_instances}
|
||||
|
||||
steps_context = []
|
||||
for step in steps:
|
||||
step_instance = next((si for si in step_instances if si.step_id == step.id), None)
|
||||
status = step_id_to_status.get(step.id, 'pending')
|
||||
|
||||
# بررسی دسترسی به مرحله
|
||||
can_access = instance.can_access_step(step)
|
||||
# مرحله انتخابشده (نمایش فعلی)
|
||||
is_selected = bool(current_step and step.id == current_step.id)
|
||||
# مرحلهای که باید انجام شود (مرحله جاری در instance)
|
||||
is_todo = bool(instance.current_step and step.id == instance.current_step.id)
|
||||
|
||||
steps_context.append({
|
||||
'step': step,
|
||||
'status': status,
|
||||
'can_access': can_access,
|
||||
'is_selected': is_selected,
|
||||
'is_todo': is_todo,
|
||||
'step_instance': step_instance,
|
||||
})
|
||||
|
||||
return {
|
||||
'instance': instance,
|
||||
'steps_context': steps_context,
|
||||
}
|
|
@ -4,9 +4,21 @@ from . import views
|
|||
app_name = 'processes'
|
||||
|
||||
urlpatterns = [
|
||||
# Requests UI
|
||||
path('requests/', views.request_list, name='request_list'),
|
||||
path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'),
|
||||
path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'),
|
||||
path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'),
|
||||
path('requests/<int:instance_id>/delete/', views.delete_request, name='delete_request'),
|
||||
|
||||
# New step-based architecture
|
||||
path('instance/<int:instance_id>/steps/', views.instance_steps, name='instance_steps'),
|
||||
path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
|
||||
|
||||
# Legacy process views
|
||||
path('', views.process_list, name='process_list'),
|
||||
path('<int:process_id>/', views.process_detail, name='process_detail'),
|
||||
path('<int:process_id>/start/', views.start_process, name='start_process'),
|
||||
path('instance/<int:instance_id>/', views.instance_detail, name='instance_detail'),
|
||||
path('my-processes/', views.my_processes, name='my_processes'),
|
||||
]
|
||||
]
|
|
@ -1,10 +1,21 @@
|
|||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
import json
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Process, ProcessInstance, StepInstance
|
||||
from wells.models import Well
|
||||
from accounts.models import Profile
|
||||
from .forms import ProcessInstanceForm
|
||||
from accounts.forms import CustomerForm
|
||||
from wells.forms import WellForm
|
||||
from wells.models import WaterMeterManufacturer
|
||||
|
||||
|
||||
@login_required
|
||||
def process_list(request):
|
||||
|
@ -22,6 +33,272 @@ def process_detail(request, process_id):
|
|||
'process': process
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def request_list(request):
|
||||
"""نمایش لیست درخواستها با جدول و مدال ایجاد"""
|
||||
instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').filter(is_deleted=False).order_by('-created')
|
||||
processes = Process.objects.filter(is_active=True)
|
||||
manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
|
||||
return render(request, 'processes/request_list.html', {
|
||||
'instances': instances,
|
||||
'customer_form': CustomerForm(),
|
||||
'well_form': WellForm(),
|
||||
'processes': processes,
|
||||
'manufacturers': manufacturers
|
||||
})
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def lookup_well_by_subscription(request):
|
||||
sub = request.GET.get('water_subscription_number', '').strip()
|
||||
if not sub:
|
||||
return JsonResponse({'ok': False, 'error': 'شماره اشتراک الزامی است'}, status=400)
|
||||
try:
|
||||
well = Well.objects.select_related('representative', 'water_meter_manufacturer').get(water_subscription_number=sub)
|
||||
data = {
|
||||
'id': well.id,
|
||||
'water_subscription_number': well.water_subscription_number,
|
||||
'electricity_subscription_number': well.electricity_subscription_number,
|
||||
'water_meter_serial_number': well.water_meter_serial_number,
|
||||
'water_meter_old_serial_number': well.water_meter_old_serial_number,
|
||||
'water_meter_manufacturer': well.water_meter_manufacturer.id if well.water_meter_manufacturer else None,
|
||||
'utm_x': str(well.utm_x) if well.utm_x is not None else None,
|
||||
'utm_y': str(well.utm_y) if well.utm_y is not None else None,
|
||||
'utm_zone': well.utm_zone,
|
||||
'utm_hemisphere': well.utm_hemisphere,
|
||||
'well_power': well.well_power,
|
||||
'reference_letter_number': well.reference_letter_number,
|
||||
'reference_letter_date': well.reference_letter_date.isoformat() if well.reference_letter_date else None,
|
||||
'representative_letter_file_url': well.representative_letter_file.url if well.representative_letter_file else '',
|
||||
'representative_letter_file_name': well.representative_letter_file.name.split('/')[-1] if well.representative_letter_file else '',
|
||||
'representative_id': well.representative.id if well.representative else None,
|
||||
'representative_full_name': well.representative.get_full_name() if well.representative else None,
|
||||
}
|
||||
return JsonResponse({'ok': True, 'exists': True, 'well': data})
|
||||
except Well.DoesNotExist:
|
||||
return JsonResponse({'ok': True, 'exists': False})
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def lookup_representative_by_national_code(request):
|
||||
national_code = request.GET.get('national_code', '').strip()
|
||||
if not national_code:
|
||||
return JsonResponse({'ok': False, 'error': 'کد ملی الزامی است'}, status=400)
|
||||
profile = Profile.objects.select_related('user').filter(national_code=national_code).first()
|
||||
if not profile:
|
||||
return JsonResponse({'ok': True, 'exists': False})
|
||||
user = profile.user
|
||||
return JsonResponse({
|
||||
'ok': True,
|
||||
'exists': True,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'full_name': user.get_full_name(),
|
||||
'profile': {
|
||||
'national_code': profile.national_code,
|
||||
'phone_number_1': profile.phone_number_1,
|
||||
'phone_number_2': profile.phone_number_2,
|
||||
'card_number': profile.card_number,
|
||||
'account_number': profile.account_number,
|
||||
'address': profile.address,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@transaction.atomic
|
||||
def create_request_with_entities(request):
|
||||
"""ایجاد/بهروزرسانی چاه و نماینده و سپس ایجاد درخواست"""
|
||||
User = get_user_model()
|
||||
process_id = request.POST.get('process')
|
||||
process = Process.objects.get(id=process_id)
|
||||
description = request.POST.get('description', '')
|
||||
# Well fields
|
||||
water_subscription_number = request.POST.get('water_subscription_number')
|
||||
well_id = request.POST.get('well_id') # optional if existing
|
||||
# Representative fields
|
||||
representative_id = request.POST.get('representative_id')
|
||||
# Prefer plain CustomerForm keys; fallback to representative_* keys
|
||||
representative_national_code = request.POST.get('national_code') or request.POST.get('representative_national_code')
|
||||
representative_first_name = request.POST.get('first_name') or request.POST.get('representative_first_name')
|
||||
representative_last_name = request.POST.get('last_name') or request.POST.get('representative_last_name')
|
||||
representative_username = request.POST.get('username') or request.POST.get('representative_username')
|
||||
representative_phone_number_1 = request.POST.get('phone_number_1') or request.POST.get('representative_phone_number_1')
|
||||
representative_phone_number_2 = request.POST.get('phone_number_2') or request.POST.get('representative_phone_number_2')
|
||||
representative_card_number = request.POST.get('card_number') or request.POST.get('representative_card_number')
|
||||
representative_account_number = request.POST.get('account_number') or request.POST.get('representative_account_number')
|
||||
representative_address = request.POST.get('address') or request.POST.get('representative_address')
|
||||
|
||||
if not process_id:
|
||||
return JsonResponse({'ok': False, 'errors': {'request': {'process': ['فرآیند الزامی است']}}}, status=400)
|
||||
if not water_subscription_number:
|
||||
return JsonResponse({'ok': False, 'errors': {'well': {'water_subscription_number': ['شماره اشتراک آب الزامی است']}}}, status=400)
|
||||
if not representative_id and not representative_national_code:
|
||||
return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400)
|
||||
|
||||
representative_user = None
|
||||
representative_profile = None
|
||||
if representative_id:
|
||||
representative_profile = Profile.objects.select_related('user').filter(user_id=representative_id).first()
|
||||
if not representative_profile:
|
||||
return JsonResponse({'ok': False, 'errors': {'customer': {'__all__': ['نماینده انتخابشده یافت نشد']}}}, status=400)
|
||||
representative_user = representative_profile.user
|
||||
# Optionally update if fields provided
|
||||
changed = False
|
||||
if representative_first_name:
|
||||
representative_user.first_name = representative_first_name
|
||||
changed = True
|
||||
if representative_last_name:
|
||||
representative_user.last_name = representative_last_name
|
||||
changed = True
|
||||
if representative_username:
|
||||
representative_user.username = representative_username
|
||||
changed = True
|
||||
if changed:
|
||||
representative_user.save()
|
||||
if representative_national_code:
|
||||
representative_profile.national_code = representative_national_code
|
||||
if representative_phone_number_1 is not None:
|
||||
representative_profile.phone_number_1 = representative_phone_number_1
|
||||
if representative_phone_number_2 is not None:
|
||||
representative_profile.phone_number_2 = representative_phone_number_2
|
||||
if representative_card_number is not None:
|
||||
representative_profile.card_number = representative_card_number
|
||||
if representative_account_number is not None:
|
||||
representative_profile.account_number = representative_account_number
|
||||
if representative_address is not None:
|
||||
representative_profile.address = representative_address
|
||||
representative_profile.save()
|
||||
else:
|
||||
# Use CustomerForm to validate/create/update representative profile by national code
|
||||
profile_instance = None
|
||||
if representative_national_code:
|
||||
profile_instance = Profile.objects.filter(national_code=representative_national_code).first()
|
||||
customer_data = {
|
||||
'first_name': representative_first_name or '',
|
||||
'last_name': representative_last_name or '',
|
||||
'phone_number_1': representative_phone_number_1 or '',
|
||||
'phone_number_2': representative_phone_number_2 or '',
|
||||
'national_code': representative_national_code or '',
|
||||
'address': representative_address or '',
|
||||
'card_number': representative_card_number or '',
|
||||
'account_number': representative_account_number or '',
|
||||
}
|
||||
customer_form = CustomerForm(customer_data, instance=profile_instance)
|
||||
customer_form.request = request
|
||||
if not customer_form.is_valid():
|
||||
return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
|
||||
representative_profile = customer_form.save()
|
||||
representative_user = representative_profile.user
|
||||
|
||||
# Resolve/create/update well
|
||||
# Build WellForm data from POST
|
||||
well = None
|
||||
if well_id:
|
||||
well = Well.objects.filter(id=well_id).first()
|
||||
if not well:
|
||||
return JsonResponse({'ok': False, 'error': 'شناسه چاه نامعتبر است'}, status=400)
|
||||
else:
|
||||
existing = Well.objects.filter(water_subscription_number=water_subscription_number).first()
|
||||
if existing:
|
||||
well = existing
|
||||
|
||||
well_data = request.POST.copy()
|
||||
# Ensure representative set from created/selected user if not provided
|
||||
if representative_user and not well_data.get('representative'):
|
||||
well_data['representative'] = str(representative_user.id)
|
||||
if not well_data.get('water_subscription_number'):
|
||||
well_data['water_subscription_number'] = water_subscription_number
|
||||
|
||||
# Preserve existing values on partial updates
|
||||
if well:
|
||||
for field_name in WellForm.Meta.fields:
|
||||
if field_name in ('representative_letter_file',):
|
||||
# File field handled via request.FILES; skip if not provided
|
||||
continue
|
||||
incoming = well_data.get(field_name, None)
|
||||
if incoming is None or incoming == '':
|
||||
current_value = getattr(well, field_name, None)
|
||||
if current_value is None:
|
||||
continue
|
||||
# Convert FK to id
|
||||
if hasattr(current_value, 'pk'):
|
||||
well_data[field_name] = str(current_value.pk)
|
||||
else:
|
||||
# Convert dates/decimals/others to string
|
||||
try:
|
||||
well_data[field_name] = current_value.isoformat() # dates
|
||||
except AttributeError:
|
||||
well_data[field_name] = str(current_value)
|
||||
|
||||
well_form = WellForm(well_data, request.FILES, instance=well)
|
||||
if not well_form.is_valid():
|
||||
return JsonResponse({'ok': False, 'errors': {'well': well_form.errors}}, status=400)
|
||||
# Save with ability to remove existing file
|
||||
well = well_form.save(commit=False)
|
||||
try:
|
||||
if request.POST.get('remove_file') == 'true' and getattr(well, 'representative_letter_file', None):
|
||||
well.representative_letter_file.delete(save=False)
|
||||
well.representative_letter_file = None
|
||||
except Exception:
|
||||
pass
|
||||
well.save()
|
||||
# Auto fill geo ownership from current user profile if available
|
||||
current_profile = getattr(request.user, 'profile', None)
|
||||
if current_profile:
|
||||
if hasattr(well, 'affairs'):
|
||||
well.affairs = current_profile.affairs
|
||||
if hasattr(well, 'county'):
|
||||
well.county = current_profile.county
|
||||
if hasattr(well, 'broker'):
|
||||
well.broker = current_profile.broker
|
||||
well.save()
|
||||
|
||||
# Create request instance
|
||||
instance = ProcessInstance.objects.create(
|
||||
process=process,
|
||||
description=description,
|
||||
well=well,
|
||||
representative=representative_user,
|
||||
requester=request.user,
|
||||
status='pending',
|
||||
priority='medium',
|
||||
)
|
||||
# ایجاد نمونههای مرحله بر اساس مراحل فرآیند و تنظیم مرحله فعلی
|
||||
for step in process.steps.all().order_by('order'):
|
||||
StepInstance.objects.create(
|
||||
process_instance=instance,
|
||||
step=step
|
||||
)
|
||||
first_step = process.steps.all().order_by('order').first()
|
||||
if first_step:
|
||||
instance.current_step = first_step
|
||||
instance.status = 'in_progress'
|
||||
instance.save()
|
||||
|
||||
redirect_url = reverse('processes:instance_steps', args=[instance.id])
|
||||
return JsonResponse({'ok': True, 'instance_id': instance.id, 'redirect': redirect_url})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def delete_request(request, instance_id):
|
||||
"""حذف درخواست"""
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
code = instance.code
|
||||
instance.delete()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'درخواست {code} با موفقیت حذف شد'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def start_process(request, process_id):
|
||||
"""شروع فرآیند جدید"""
|
||||
|
@ -67,6 +344,61 @@ def instance_detail(request, instance_id):
|
|||
'instance': instance
|
||||
})
|
||||
|
||||
@login_required
|
||||
def step_detail(request, instance_id, step_id):
|
||||
"""نمایش جزئیات مرحله خاص"""
|
||||
instance = get_object_or_404(
|
||||
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
|
||||
id=instance_id
|
||||
)
|
||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||
|
||||
# بررسی دسترسی به مرحله
|
||||
if not instance.can_access_step(step):
|
||||
messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
|
||||
return redirect('processes:request_list')
|
||||
|
||||
# هدایت به view مناسب بر اساس نوع مرحله
|
||||
if step.order == 1: # مرحله اول - انتخاب اقلام
|
||||
return redirect('invoices:quote_step', instance_id=instance.id, step_id=step.id)
|
||||
elif step.order == 2: # مرحله دوم - صدور پیشفاکتور
|
||||
return redirect('invoices:quote_preview_step', instance_id=instance.id, step_id=step.id)
|
||||
elif step.order == 3: # مرحله سوم - ثبت فیشهای واریزی
|
||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||
|
||||
# برای سایر مراحل، template عمومی نمایش داده میشود
|
||||
step_instance = instance.step_instances.filter(step=step).first()
|
||||
|
||||
# Navigation logic
|
||||
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
||||
|
||||
return render(request, 'processes/step_detail.html', {
|
||||
'instance': instance,
|
||||
'step': step,
|
||||
'step_instance': step_instance,
|
||||
'previous_step': previous_step,
|
||||
'next_step': next_step,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def instance_steps(request, instance_id):
|
||||
"""هدایت به مرحله فعلی instance"""
|
||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||
|
||||
if not instance.current_step:
|
||||
# اگر مرحله فعلی تعریف نشده، به اولین مرحله برو
|
||||
first_step = instance.process.steps.first()
|
||||
if first_step:
|
||||
instance.current_step = first_step
|
||||
instance.save()
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=first_step.id)
|
||||
else:
|
||||
messages.error(request, 'هیچ مرحلهای برای این فرآیند تعریف نشده است.')
|
||||
return redirect('processes:request_list')
|
||||
|
||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
||||
|
||||
@login_required
|
||||
def my_processes(request):
|
||||
"""نمایش فرآیندهای کاربر"""
|
||||
|
@ -76,4 +408,5 @@ def my_processes(request):
|
|||
return render(request, 'processes/my_processes.html', {
|
||||
'my_instances': my_instances,
|
||||
'assigned_steps': assigned_steps
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue