From 6f3ce51ab9f18094c5e05e763aa7f893c831c341 Mon Sep 17 00:00:00 2001 From: aminhashemi92 Date: Sun, 7 Sep 2025 11:06:21 +0330 Subject: [PATCH] clean up proccess and req_list app. --- accounts/migrations/0001_initial.py | 25 +- accounts/migrations/0002_company.py | 34 --- ...icalprofile_bank_name_profile_bank_name.py | 23 -- certificates/migrations/0001_initial.py | 7 +- ...rtificatetemplate_company_logo_and_more.py | 32 -- contracts/migrations/0001_initial.py | 64 +--- ...lcontracttemplate_history_user_and_more.py | 38 --- db.sqlite3 | Bin 2314240 -> 2375680 bytes installations/migrations/0001_initial.py | 6 +- ...02_installationreport_approved_and_more.py | 23 -- invoices/migrations/0001_initial.py | 12 +- ...istoricalpayment_receipt_image_and_more.py | 23 -- ...oricalpayment_reference_number_and_more.py | 23 -- ...icalpayment_direction_payment_direction.py | 23 -- ...0005_historicalitem_is_special_and_more.py | 33 -- ...ve_historicalitem_special_kind_and_more.py | 21 -- locations/migrations/0001_initial.py | 2 +- processes/admin.py | 14 +- processes/forms.py | 26 -- processes/migrations/0001_initial.py | 59 ++-- ...02_stepapproval_stepapproverrequirement.py | 48 --- processes/models.py | 38 +-- .../templates/processes/request_list.html | 287 +++++++++++------- processes/urls.py | 6 - processes/views.py | 162 ++-------- wells/migrations/0001_initial.py | 2 +- 26 files changed, 287 insertions(+), 744 deletions(-) delete mode 100644 accounts/migrations/0002_company.py delete mode 100644 accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py delete mode 100644 certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py delete mode 100644 contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py delete mode 100644 installations/migrations/0002_installationreport_approved_and_more.py delete mode 100644 invoices/migrations/0002_historicalpayment_receipt_image_and_more.py delete mode 100644 invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py delete mode 100644 invoices/migrations/0004_historicalpayment_direction_payment_direction.py delete mode 100644 invoices/migrations/0005_historicalitem_is_special_and_more.py delete mode 100644 invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py delete mode 100644 processes/migrations/0002_stepapproval_stepapproverrequirement.py diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index de431d4..7b35fb7 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -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.core.validators import django.db.models.deletion @@ -17,6 +17,27 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')), + ('updated', models.DateTimeField(auto_now=True, 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, unique=True, verbose_name='اسلاگ')), + ('name', models.CharField(max_length=100, verbose_name='نام')), + ('logo', models.ImageField(blank=True, null=True, upload_to='companies/logos', verbose_name='لوگوی شرکت')), + ('signature', models.ImageField(blank=True, null=True, upload_to='companies/signatures', verbose_name='امضای شرکت')), + ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), + ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس')), + ], + options={ + 'verbose_name': 'شرکت', + 'verbose_name_plural': 'شرکت\u200cها', + }, + ), migrations.CreateModel( name='HistoricalProfile', fields=[ @@ -30,6 +51,7 @@ class Migration(migrations.Migration): ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')), ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')), + ('bank_name', models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک')), ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')), ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')), ('pic', models.TextField(default='../static/sample_images/profile.jpg', max_length=100, verbose_name='تصویر')), @@ -84,6 +106,7 @@ class Migration(migrations.Migration): ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')), ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')), + ('bank_name', models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک')), ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')), ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')), ('pic', models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر')), diff --git a/accounts/migrations/0002_company.py b/accounts/migrations/0002_company.py deleted file mode 100644 index c944cdf..0000000 --- a/accounts/migrations/0002_company.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-21 06:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Company', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')), - ('updated', models.DateTimeField(auto_now=True, 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, unique=True, verbose_name='اسلاگ')), - ('name', models.CharField(max_length=100, verbose_name='نام')), - ('logo', models.ImageField(blank=True, null=True, upload_to='companies/logos', verbose_name='لوگوی شرکت')), - ('signature', models.ImageField(blank=True, null=True, upload_to='companies/signatures', verbose_name='امضای شرکت')), - ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), - ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس')), - ], - options={ - 'verbose_name': 'شرکت', - 'verbose_name_plural': 'شرکت\u200cها', - }, - ), - ] diff --git a/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py b/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py deleted file mode 100644 index 6becfec..0000000 --- a/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-21 07:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_company'), - ] - - operations = [ - migrations.AddField( - model_name='historicalprofile', - name='bank_name', - field=models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک'), - ), - migrations.AddField( - model_name='profile', - name='bank_name', - field=models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک'), - ), - ] diff --git a/certificates/migrations/0001_initial.py b/certificates/migrations/0001_initial.py index 83533bd..b5753ec 100644 --- a/certificates/migrations/0001_initial.py +++ b/certificates/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-08-22 09:58 +# Generated by Django 5.2.4 on 2025-09-07 07:35 import django.db.models.deletion from django.db import migrations, models @@ -9,6 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('accounts', '0001_initial'), ('processes', '0001_initial'), ] @@ -23,10 +24,8 @@ class Migration(migrations.Migration): ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')), ('title', models.CharField(max_length=200, verbose_name='عنوان')), ('body', models.TextField(verbose_name='متن قالب (با جایگزین\u200cها)')), - ('company_logo', models.ImageField(blank=True, null=True, upload_to='certificates/logos/%Y/%m/%d/', verbose_name='لوگو')), - ('company_name', models.CharField(blank=True, max_length=200, verbose_name='نام شرکت')), - ('company_seal_signature', models.ImageField(blank=True, null=True, upload_to='certificates/seals/%Y/%m/%d/', verbose_name='مهر و امضا')), ('is_active', models.BooleanField(default=True, verbose_name='فعال')), + ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت صادر کننده')), ], options={ 'verbose_name': 'قالب گواهی', diff --git a/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py b/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py deleted file mode 100644 index e929c5a..0000000 --- a/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-22 10:05 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0003_historicalprofile_bank_name_profile_bank_name'), - ('certificates', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='certificatetemplate', - name='company_logo', - ), - migrations.RemoveField( - model_name='certificatetemplate', - name='company_name', - ), - migrations.RemoveField( - model_name='certificatetemplate', - name='company_seal_signature', - ), - migrations.AddField( - model_name='certificatetemplate', - name='company', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت صادر کننده'), - ), - ] diff --git a/contracts/migrations/0001_initial.py b/contracts/migrations/0001_initial.py index 8f9f4b7..25acea6 100644 --- a/contracts/migrations/0001_initial.py +++ b/contracts/migrations/0001_initial.py @@ -1,7 +1,6 @@ -# Generated by Django 5.2.4 on 2025-08-21 06:00 +# Generated by Django 5.2.4 on 2025-09-07 07:35 import django.db.models.deletion -import simple_history.models from django.conf import settings from django.db import migrations, models @@ -11,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('accounts', '0001_initial'), ('processes', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -28,8 +28,7 @@ class Migration(migrations.Migration): ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')), ('name', models.CharField(max_length=100, verbose_name='نام')), ('body', models.TextField(verbose_name='متن قرارداد')), - ('company_logo', models.ImageField(blank=True, null=True, upload_to='contracts/logos/%Y/%m/%d/', verbose_name='لوگوی شرکت')), - ('company_signature', models.ImageField(blank=True, null=True, upload_to='contracts/signatures/%Y/%m/%d/', verbose_name='امضای شرکت')), + ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت')), ], options={ 'verbose_name': 'قالب قرارداد', @@ -58,61 +57,4 @@ class Migration(migrations.Migration): 'ordering': ['-created'], }, ), - migrations.CreateModel( - name='HistoricalContractInstance', - 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='تاریخ حذف')), - ('rendered_body', models.TextField(verbose_name='متن نهایی قرارداد')), - ('approved', models.BooleanField(default=False, verbose_name='تایید شده')), - ('approved_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)), - ('created_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='ایجاد کننده')), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')), - ('template', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='contracts.contracttemplate', verbose_name='قالب مورد استفاده')), - ], - options={ - 'verbose_name': 'historical قرارداد', - 'verbose_name_plural': 'historical قراردادها', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), - }, - bases=(simple_history.models.HistoricalChanges, models.Model), - ), - migrations.CreateModel( - name='HistoricalContractTemplate', - 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='نام')), - ('body', models.TextField(verbose_name='متن قرارداد')), - ('company_logo', models.TextField(blank=True, max_length=100, null=True, verbose_name='لوگوی شرکت')), - ('company_signature', models.TextField(blank=True, max_length=100, 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)), - ], - 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), - ), ] diff --git a/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py b/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py deleted file mode 100644 index 60d434d..0000000 --- a/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-21 06:33 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_company'), - ('contracts', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='historicalcontracttemplate', - name='history_user', - ), - migrations.RemoveField( - model_name='contracttemplate', - name='company_logo', - ), - migrations.RemoveField( - model_name='contracttemplate', - name='company_signature', - ), - migrations.AddField( - model_name='contracttemplate', - name='company', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت'), - ), - migrations.DeleteModel( - name='HistoricalContractInstance', - ), - migrations.DeleteModel( - name='HistoricalContractTemplate', - ), - ] diff --git a/db.sqlite3 b/db.sqlite3 index d0474e889234d6c65d345ece284a7c06eca8a071..d816e90a68a8de8a09df9d5f29695f3bc6f73264 100644 GIT binary patch delta 31676 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 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): diff --git a/processes/forms.py b/processes/forms.py index 534c475..e69de29 100644 --- a/processes/forms.py +++ b/processes/forms.py @@ -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}) - } \ No newline at end of file diff --git a/processes/migrations/0001_initial.py b/processes/migrations/0001_initial.py index 8675e70..4e90bf7 100644 --- a/processes/migrations/0001_initial.py +++ b/processes/migrations/0001_initial.py @@ -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')}, + }, + ), ] diff --git a/processes/migrations/0002_stepapproval_stepapproverrequirement.py b/processes/migrations/0002_stepapproval_stepapproverrequirement.py deleted file mode 100644 index 6a771b1..0000000 --- a/processes/migrations/0002_stepapproval_stepapproverrequirement.py +++ /dev/null @@ -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')}, - }, - ), - ] diff --git a/processes/models.py b/processes/models.py index 604bb7b..0a119db 100644 --- a/processes/models.py +++ b/processes/models.py @@ -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.""" diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index 6ca6d95..5a0331f 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -28,17 +28,113 @@

-
-

درخواست‌ها

- +
+
+
لیست درخواست‌ها
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ کل درخواست‌ها +
+

{{ total_count }}

+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ تکمیل‌شده +
+

{{ completed_count }}

+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ در حال انجام +
+

{{ in_progress_count }}

+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ در انتظار +
+

{{ pending_count }}

+
+
+
+ + + +
+
+
+
+
-
- +
+
@@ -46,8 +142,8 @@ - - + + @@ -61,9 +157,9 @@ - - - + + +
شناسهمرحله فعلی شماره اشتراک آب نمایندهدرخواست‌کنندهاولویتاستانامور وضعیت تاریخ ایجاد عملیات{{ inst.current_step.name|default:"--" }} {{ inst.well.water_subscription_number }} {% if inst.representative %}{{ inst.representative.get_full_name }}{% else %}-{% endif %}{% if inst.requester %}{{ inst.requester.get_full_name }}{% else %}-{% endif %}{{ inst.get_priority_display }}{{ inst.get_status_display }}{% if inst.well and inst.well.county %}{{ inst.well.county }}{% else %}-{% endif %}{% if inst.well and inst.well.affairs %}{{ inst.well.affairs }}{% else %}-{% endif %}{{ inst.get_status_display_with_color|safe }} {{ inst.jcreated }}
@@ -126,7 +222,7 @@
- + @@ -217,7 +313,7 @@
- + @@ -268,7 +364,7 @@
- +
@@ -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 = $('
').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(); diff --git a/processes/urls.py b/processes/urls.py index fc2c424..a58ebab 100644 --- a/processes/urls.py +++ b/processes/urls.py @@ -16,10 +16,4 @@ urlpatterns = [ path('instance//step//', views.step_detail, name='step_detail'), path('instance//summary/', views.instance_summary, name='instance_summary'), - # Legacy process views - path('', views.process_list, name='process_list'), - path('/', views.process_detail, name='process_detail'), - path('/start/', views.start_process, name='start_process'), - path('instance//', views.instance_detail, name='instance_detail'), - path('my-processes/', views.my_processes, name='my_processes'), ] \ No newline at end of file diff --git a/processes/views.py b/processes/views.py index 9cbb687..8f62913 100644 --- a/processes/views.py +++ b/processes/views.py @@ -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 - }) - + \ No newline at end of file diff --git a/wells/migrations/0001_initial.py b/wells/migrations/0001_initial.py index 838ca14..46bdd36 100644 --- a/wells/migrations/0001_initial.py +++ b/wells/migrations/0001_initial.py @@ -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