clean up proccess and req_list app.

This commit is contained in:
aminhashemi92 2025-09-07 11:06:21 +03:30
parent 35799b7754
commit 6f3ce51ab9
26 changed files with 287 additions and 744 deletions

View file

@ -2,7 +2,7 @@ from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision, StepApproverRequirement, StepApproval
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepApproverRequirement, StepApproval
@admin.register(Process)
class ProcessAdmin(SimpleHistoryAdmin):
@ -168,18 +168,6 @@ class StepRejectionAdmin(SimpleHistoryAdmin):
return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason
reason_short.short_description = "دلیل رد شدن"
@admin.register(StepRevision)
class StepRevisionAdmin(SimpleHistoryAdmin):
list_display = ['step_instance', 'rejection', 'revised_by', 'changes_short', 'created_at']
list_filter = ['revised_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'revised_by__username', 'changes_description']
readonly_fields = ['created_at']
ordering = ['-created_at']
def changes_short(self, obj):
return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description
changes_short.short_description = "تغییرات"
@admin.register(StepApproverRequirement)
class StepApproverRequirementAdmin(admin.ModelAdmin):

View file

@ -1,26 +0,0 @@
from django import forms
from .models import ProcessInstance, StepInstance
class ProcessInstanceForm(forms.ModelForm):
class Meta:
model = ProcessInstance
fields = ['description', 'process', 'well', 'representative', 'requester', 'priority', 'status', 'current_step']
widgets = {
'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):
class Meta:
model = StepInstance
fields = ['status', 'notes']
widgets = {
'status': forms.Select(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3})
}

View file

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-14 09:02
# Generated by Django 5.2.4 on 2025-09-07 07:35
import django.db.models.deletion
import simple_history.models
@ -11,6 +11,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
('wells', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@ -231,42 +232,17 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='HistoricalStepRevision',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
('created_at', models.DateTimeField(blank=True, editable=False, 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)),
('revised_by', 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='اصلاح کننده')),
('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')),
('rejection', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.steprejection', 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='StepRevision',
name='StepApproverRequirement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ اصلاح')),
('rejection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.steprejection', verbose_name='رد شدن مربوطه')),
('revised_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_revisions', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')),
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.stepinstance', verbose_name='نمونه مرحله')),
('required_count', models.PositiveIntegerField(default=1, verbose_name='تعداد موردنیاز')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش تاییدکننده')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approver_requirements', to='processes.processstep', verbose_name='مرحله')),
],
options={
'verbose_name': 'بازبینی مرحله',
'verbose_name_plural': 'بازبینی\u200cهای مراحل',
'ordering': ['-created_at'],
'verbose_name': 'نیازمندی تایید نقش',
'verbose_name_plural': 'نیازمندی\u200cهای تایید نقش',
'unique_together': {('step', 'role')},
},
),
migrations.CreateModel(
@ -284,4 +260,21 @@ class Migration(migrations.Migration):
'unique_together': {('dependent_step', 'dependency_step')},
},
),
migrations.CreateModel(
name='StepApproval',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('decision', models.CharField(choices=[('approved', 'تایید'), ('rejected', 'رد')], max_length=8, verbose_name='نتیجه')),
('reason', models.TextField(blank=True, verbose_name='علت (برای رد)')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')),
('approved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='تاییدکننده')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش')),
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approvals', to='processes.stepinstance', verbose_name='نمونه مرحله')),
],
options={
'verbose_name': 'تایید مرحله',
'verbose_name_plural': 'تاییدهای مرحله',
'unique_together': {('step_instance', 'role')},
},
),
]

View file

@ -1,48 +0,0 @@
# Generated by Django 5.2.4 on 2025-09-01 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_historicalprofile_bank_name_profile_bank_name'),
('processes', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='StepApproval',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('decision', models.CharField(choices=[('approved', 'تایید'), ('rejected', 'رد')], max_length=8, verbose_name='نتیجه')),
('reason', models.TextField(blank=True, verbose_name='علت (برای رد)')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')),
('approved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='تاییدکننده')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش')),
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approvals', to='processes.stepinstance', verbose_name='نمونه مرحله')),
],
options={
'verbose_name': 'تایید مرحله',
'verbose_name_plural': 'تاییدهای مرحله',
'unique_together': {('step_instance', 'role')},
},
),
migrations.CreateModel(
name='StepApproverRequirement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('required_count', models.PositiveIntegerField(default=1, verbose_name='تعداد موردنیاز')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش تاییدکننده')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approver_requirements', to='processes.processstep', verbose_name='مرحله')),
],
options={
'verbose_name': 'نیازمندی تایید نقش',
'verbose_name_plural': 'نیازمندی\u200cهای تایید نقش',
'unique_together': {('step', 'role')},
},
),
]

View file

@ -68,6 +68,7 @@ class ProcessStep(NameSlugModel):
"""دریافت مراحلی که به این مرحله وابسته هستند"""
return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
class StepDependency(models.Model):
"""مدل وابستگی بین مراحل"""
dependent_step = models.ForeignKey(
@ -295,6 +296,7 @@ class ProcessInstance(SluggedModel):
return False
return True
class StepInstance(models.Model):
"""مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند")
@ -378,6 +380,7 @@ class StepInstance(models.Model):
return False
return True
class StepRejection(models.Model):
"""مدل رد شدن مرحله"""
step_instance = models.ForeignKey(
@ -415,41 +418,6 @@ class StepRejection(models.Model):
self.step_instance.save()
super().save(*args, **kwargs)
class StepRevision(models.Model):
"""مدل بازبینی و اصلاح مرحله"""
step_instance = models.ForeignKey(
StepInstance,
on_delete=models.CASCADE,
related_name='revisions',
verbose_name="نمونه مرحله"
)
rejection = models.ForeignKey(
StepRejection,
on_delete=models.CASCADE,
related_name='revisions',
verbose_name="رد شدن مربوطه"
)
revised_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="اصلاح کننده",
related_name='step_revisions'
)
changes_description = models.TextField(
verbose_name="توضیح تغییرات",
help_text="توضیح تغییراتی که برای اصلاح انجام شده"
)
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.revised_by.get_full_name()}"
class StepApproverRequirement(models.Model):
"""Required approver roles for a step."""

View file

@ -28,17 +28,113 @@
<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 class="row py-3 mb-4 card-header flex-column flex-md-row pb-0">
<div class="d-md-flex justify-content-between align-items-center dt-layout-start col-md-auto me-auto mt-0">
<h5 class="card-title mb-0 text-md-start text-center fw-bold">لیست درخواست‌ها</h5>
</div>
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
<div class="dt-buttons btn-group flex-wrap mb-0">
<div class="btn-group">
<button class="btn buttons-collection btn-label-primary dropdown-toggle me-4 d-none" type="button">
<span>
<span class="d-flex align-items-center gap-2">
<i class="icon-base bx bx-export me-sm-1"></i>
<span class="d-none d-sm-inline-block">خروجی</span>
</span>
</span>
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
<i class="bx bx-plus me-1"></i>
درخواست جدید
</button>
</div>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row g-4 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between">
<div class="content-left">
<span>کل درخواست‌ها</span>
<div class="d-flex align-items-end mt-2">
<h4 class="mb-0 me-2">{{ total_count }}</h4>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-primary">
<i class="bx bx-list-ul bx-sm"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between">
<div class="content-left">
<span>تکمیل‌شده</span>
<div class="d-flex align-items-end mt-2">
<h4 class="mb-0 me-2">{{ completed_count }}</h4>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-success">
<i class="bx bx-badge-check bx-sm"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between">
<div class="content-left">
<span>در حال انجام</span>
<div class="d-flex align-items-end mt-2">
<h4 class="mb-0 me-2">{{ in_progress_count }}</h4>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-info">
<i class="bx bx-loader-circle bx-sm"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between">
<div class="content-left">
<span>در انتظار</span>
<div class="d-flex align-items-end mt-2">
<h4 class="mb-0 me-2">{{ pending_count }}</h4>
</div>
</div>
<div class="avatar">
<span class="avatar-initial rounded bg-label-warning">
<i class="bx bx-time bx-sm"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table id="requestsTable" class="table table-striped">
<div class="card-datatable table-responsive">
<table id="requests-table" class="datatables-basic table border-top">
<thead>
<tr>
<th>شناسه</th>
@ -46,8 +142,8 @@
<th>مرحله فعلی</th>
<th>شماره اشتراک آب</th>
<th>نماینده</th>
<th>درخواست‌کننده</th>
<th>اولویت</th>
<th>استان</th>
<th>امور</th>
<th>وضعیت</th>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
@ -61,9 +157,9 @@
<td class="text-primary">{{ inst.current_step.name|default:"--" }}</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>{% if inst.well and inst.well.county %}{{ inst.well.county }}{% else %}-{% endif %}</td>
<td>{% if inst.well and inst.well.affairs %}{{ inst.well.affairs }}{% else %}-{% endif %}</td>
<td>{{ inst.get_status_display_with_color|safe }}</td>
<td>{{ inst.jcreated }}</td>
<td>
<div class="d-inline-block">
@ -126,7 +222,7 @@
<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>
<input type="text" class="form-control" id="req_water_sub" name="water_subscription_number" data-field="water_subscription_number" placeholder="مثال: 12345" required>
<button class="btn btn-outline-secondary" type="button" id="btnLookupWell">
بررسی/افزودن چاه
</button>
@ -217,7 +313,7 @@
<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">
<input type="text" class="form-control" id="rep_national_code" data-field="national_code" placeholder="مثال: 0012345678" maxlength="10" inputmode="numeric" pattern="\d*">
<button class="btn btn-outline-secondary" type="button" id="btnLookupRep">
بررسی/افزودن نماینده
</button>
@ -268,7 +364,7 @@
<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>
<textarea class="form-control" rows="3" id="req_description" name="description"></textarea>
</div>
</div>
</form>
@ -361,19 +457,13 @@
$(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');
// }
// Initialize DataTable similar to customer_list
$('#requests-table').DataTable({
pageLength: 10,
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
order: [[0, 'desc']],
responsive: true,
});
let currentWellId = null;
let currentRepId = null;
let wellChecked = false;
@ -399,7 +489,7 @@
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');
const $grp = $el.closest('.input-group, .form-group, .mb-3');
if ($grp.length) {
$feedback.insertAfter($grp);
} else {
@ -408,60 +498,49 @@
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;
}
}
// Generic field resolution with small exception map
const exceptionMap = {
water_subscription_number: '#req_water_sub',
national_code: '#rep_national_code',
representative: '#rep_national_code'
};
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 'bank_name': return '#id_bank_name';
case 'address': return '#id_address';
default: return '#id_' + field;
}
function findFieldSelector(field, context) {
const $ctx = context ? $(context) : $('#requestModal');
let $el = $ctx.find(`#id_${field}`).first();
if ($el.length) return $el;
$el = $ctx.find(`[name="${field}"]`).first();
if ($el.length) return $el;
$el = $ctx.find(`[data-field="${field}"]`).first();
if ($el.length) return $el;
const ex = exceptionMap[field];
return ex ? $(ex) : $();
}
function showInlineErrors(errors) {
if (!errors) return;
let nonFieldWell = '';
let nonFieldCustomer = '';
// Request-level errors (e.g., process)
if (errors.request) {
for (const key in errors.request) {
const msgs = Array.isArray(errors.request[key]) ? errors.request[key] : [errors.request[key]];
if (key === '__all__' || key === 'non_field_errors') { continue; }
applyErrorTo(findFieldSelector(key, '#requestForm'), msgs[0]);
}
}
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]);
applyErrorTo(findFieldSelector(key, '#wellFormBlock'), 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]);
applyErrorTo(findFieldSelector(key, '#repNewFields'), msgs[0]);
}
}
if (nonFieldWell) setStatus('#wellStatus', nonFieldWell, 'danger');
@ -536,7 +615,7 @@
$('#remove-file').val('false');
// Initialize Persian Date Picker after well form is shown
setTimeout(initPersianDatePicker, 100);
setStatus('#wellStatus', 'چاه یافت نشد. با ذخیره، ایجاد خواهد شد.', 'danger');
setStatus('#wellStatus', 'چاه یافت نشد. اطلاعات چاه را وارد کنید.', 'danger');
}
})
.fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
@ -594,46 +673,26 @@
});
$('#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());
clearInlineErrors();
// Use form's native FormData - much cleaner!
const formData = new FormData(document.getElementById('requestForm'));
// Add custom fields that aren't in the form
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() || '');
formData.append('bank_name', $('#id_bank_name').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]);
}
// Handle special national_code logic (prefer visible field)
const ncField = $('#id_national_code').val();
if (ncField) {
formData.set('national_code', ncField);
} else {
formData.set('national_code', $('#rep_national_code').val().trim());
}
// Handle Persian date conversion
const gregorianDate = $('#id_reference_letter_date').attr('data-gregorian');
if (gregorianDate) {
formData.set('reference_letter_date', gregorianDate);
}
const $btn = $(this).prop('disabled', true).text('در حال ذخیره...');
@ -652,6 +711,10 @@
setTimeout(function(){ location.reload(); }, 1200);
}
} else {
clearInlineErrors();
if (resp.errors) {
showInlineErrors(resp.errors);
}
const msg = buildErrorMessage(resp);
showToast(msg, 'danger');
}
@ -659,6 +722,10 @@
let msg = 'خطا در ذخیره';
try {
const resp = JSON.parse(xhr.responseText);
clearInlineErrors();
if (resp && resp.errors) {
showInlineErrors(resp.errors);
}
msg = buildErrorMessage(resp) || msg;
} catch(e) {}
showToast(msg, 'danger');
@ -715,6 +782,14 @@
}
});
// Enforce digit-only and max length for national code input
$('#rep_national_code').on('input', function() {
const cleaned = (this.value || '').replace(/\D/g, '').slice(0, 10);
if (this.value !== cleaned) {
this.value = cleaned;
}
});
$('#requestModal').on('hidden.bs.modal', function(){
$('#requestForm')[0].reset();
$('#wellFormBlock').hide();

View file

@ -16,10 +16,4 @@ urlpatterns = [
path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
path('instance/<int:instance_id>/summary/', views.instance_summary, name='instance_summary'),
# 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'),
]

View file

@ -1,7 +1,6 @@
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
@ -11,41 +10,32 @@ 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):
"""نمایش لیست فرآیندهای فعال"""
processes = Process.objects.filter(is_active=True)
return render(request, 'processes/process_list.html', {
'processes': processes
})
@login_required
def process_detail(request, process_id):
"""نمایش جزئیات فرآیند"""
process = get_object_or_404(Process, id=process_id, is_active=True)
return render(request, 'processes/process_detail.html', {
'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')
# Summary stats for header cards
total_count = instances.count()
completed_count = instances.filter(status='completed').count()
in_progress_count = instances.filter(status='in_progress').count()
pending_count = instances.filter(status='pending').count()
return render(request, 'processes/request_list.html', {
'instances': instances,
'customer_form': CustomerForm(),
'well_form': WellForm(),
'processes': processes,
'manufacturers': manufacturers
'manufacturers': manufacturers,
'total_count': total_count,
'completed_count': completed_count,
'in_progress_count': in_progress_count,
'pending_count': pending_count,
})
@ -127,23 +117,12 @@ def create_request_with_entities(request):
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_bank_name = request.POST.get('bank_name') or request.POST.get('representative_bank_name')
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:
if not representative_id and not request.POST.get('national_code'):
return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400)
representative_user = None
@ -152,52 +131,20 @@ def create_request_with_entities(request):
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)
# Use CustomerForm with request.POST data, merging with existing values
customer_form = CustomerForm(request.POST, instance=representative_profile)
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
# 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_bank_name is not None:
representative_profile.bank_name = representative_bank_name
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 '',
'bank_name': representative_bank_name or '',
}
customer_form = CustomerForm(customer_data, instance=profile_instance)
national_code = request.POST.get('national_code')
if national_code:
profile_instance = Profile.objects.filter(national_code=national_code).first()
customer_form = CustomerForm(request.POST, instance=profile_instance)
customer_form.request = request
if not customer_form.is_valid():
return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
@ -292,62 +239,24 @@ def create_request_with_entities(request):
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
if instance.status == 'completed':
return JsonResponse({
'success': False,
'message': 'درخواست تکمیل شده نمی‌تواند حذف شود'
})
instance.delete()
return JsonResponse({
'success': True,
'message': f'درخواست {code} با موفقیت حذف شد'
})
@login_required
def start_process(request, process_id):
"""شروع فرآیند جدید"""
process = get_object_or_404(Process, id=process_id, is_active=True)
if request.method == 'POST':
form = ProcessInstanceForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.process = process
instance.requester = request.user
instance.save()
# ایجاد نمونه‌های مرحله
for step in process.steps.all():
StepInstance.objects.create(
process_instance=instance,
step=step
)
# تنظیم مرحله اول به عنوان مرحله فعلی
first_step = process.steps.first()
if first_step:
instance.current_step = first_step
instance.status = 'in_progress'
instance.save()
messages.success(request, f'فرآیند {process.name} با موفقیت شروع شد.')
return redirect('processes:instance_detail', instance_id=instance.id)
else:
form = ProcessInstanceForm()
return render(request, 'processes/start_process.html', {
'process': process,
'form': form
})
@login_required
def instance_detail(request, instance_id):
"""نمایش جزئیات نمونه فرآیند"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
return render(request, 'processes/instance_detail.html', {
'instance': instance
})
@login_required
def step_detail(request, instance_id, step_id):
@ -409,6 +318,7 @@ def step_detail(request, instance_id, step_id):
'next_step': next_step,
})
@login_required
def instance_steps(request, instance_id):
"""هدایت به مرحله فعلی instance"""
@ -430,6 +340,7 @@ def instance_steps(request, instance_id):
return redirect('processes:instance_summary', instance_id=instance.id)
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
@login_required
def instance_summary(request, instance_id):
"""نمای خلاصهٔ فقط‌خواندنی برای درخواست‌های تکمیل‌شده."""
@ -461,15 +372,4 @@ def instance_summary(request, instance_id):
'latest_report': latest_report,
'certificate': certificate,
})
@login_required
def my_processes(request):
"""نمایش فرآیندهای کاربر"""
my_instances = ProcessInstance.objects.filter(requester=request.user)
assigned_steps = StepInstance.objects.filter(assigned_to=request.user, status='in_progress')
return render(request, 'processes/my_processes.html', {
'my_instances': my_instances,
'assigned_steps': assigned_steps
})