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 d0474e8..d816e90 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/installations/migrations/0001_initial.py b/installations/migrations/0001_initial.py index 05cedcd..41f02f5 100644 --- a/installations/migrations/0001_initial.py +++ b/installations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-08-21 08:25 +# Generated by Django 5.2.4 on 2025-09-07 07:35 import django.db.models.deletion from django.conf import settings @@ -10,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('invoices', '0002_historicalpayment_receipt_image_and_more'), + ('invoices', '0001_initial'), ('processes', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -53,6 +53,8 @@ class Migration(migrations.Migration): ('utm_x', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='UTM X')), ('utm_y', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='UTM Y')), ('description', models.TextField(blank=True, verbose_name='توضیحات')), + ('approved', models.BooleanField(default=False, verbose_name='تایید شده')), + ('approved_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تایید')), ('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports', to='installations.installationassignment', verbose_name='اختصاص')), ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='ایجادکننده')), ], diff --git a/installations/migrations/0002_installationreport_approved_and_more.py b/installations/migrations/0002_installationreport_approved_and_more.py deleted file mode 100644 index d5df3c8..0000000 --- a/installations/migrations/0002_installationreport_approved_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-21 09:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('installations', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='installationreport', - name='approved', - field=models.BooleanField(default=False, verbose_name='تایید شده'), - ), - migrations.AddField( - model_name='installationreport', - name='approved_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تایید'), - ), - ] diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py index abf23d3..1714b64 100644 --- a/invoices/migrations/0001_initial.py +++ b/invoices/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 @@ -29,6 +29,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100, verbose_name='نام')), ('description', models.TextField(blank=True, verbose_name='توضیحات')), ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')), + ('is_special', models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی')), ('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')), ('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')), ('history_id', models.AutoField(primary_key=True, serialize=False)), @@ -121,10 +122,12 @@ class Migration(migrations.Migration): ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')), ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')), ('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')), + ('direction', models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش')), ('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')), - ('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')), + ('reference_number', models.CharField(blank=True, db_index=True, max_length=100, verbose_name='شماره مرجع')), ('payment_date', models.DateField(verbose_name='تاریخ پرداخت')), ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')), + ('receipt_image', 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)), @@ -154,6 +157,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100, verbose_name='نام')), ('description', models.TextField(blank=True, verbose_name='توضیحات')), ('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')), + ('is_special', models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی')), ('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')), ('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')), @@ -225,10 +229,12 @@ class Migration(migrations.Migration): ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')), ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')), ('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')), + ('direction', models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش')), ('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')), - ('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')), + ('reference_number', models.CharField(blank=True, max_length=100, unique=True, verbose_name='شماره مرجع')), ('payment_date', models.DateField(verbose_name='تاریخ پرداخت')), ('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')), + ('receipt_image', models.ImageField(blank=True, null=True, upload_to='payments/%Y/%m/%d/', verbose_name='تصویر فیش')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')), ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice', verbose_name='فاکتور')), ], diff --git a/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py b/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py deleted file mode 100644 index d8de4a0..0000000 --- a/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-16 04:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='historicalpayment', - name='receipt_image', - field=models.TextField(blank=True, max_length=100, null=True, verbose_name='تصویر فیش'), - ), - migrations.AddField( - model_name='payment', - name='receipt_image', - field=models.ImageField(blank=True, null=True, upload_to='payments/%Y/%m/%d/', verbose_name='تصویر فیش'), - ), - ] diff --git a/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py b/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py deleted file mode 100644 index d84cc14..0000000 --- a/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-21 18:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0002_historicalpayment_receipt_image_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='historicalpayment', - name='reference_number', - field=models.CharField(blank=True, db_index=True, max_length=100, verbose_name='شماره مرجع'), - ), - migrations.AlterField( - model_name='payment', - name='reference_number', - field=models.CharField(blank=True, max_length=100, unique=True, verbose_name='شماره مرجع'), - ), - ] diff --git a/invoices/migrations/0004_historicalpayment_direction_payment_direction.py b/invoices/migrations/0004_historicalpayment_direction_payment_direction.py deleted file mode 100644 index e3416cc..0000000 --- a/invoices/migrations/0004_historicalpayment_direction_payment_direction.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-22 08:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0003_alter_historicalpayment_reference_number_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='historicalpayment', - name='direction', - field=models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش'), - ), - migrations.AddField( - model_name='payment', - name='direction', - field=models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش'), - ), - ] diff --git a/invoices/migrations/0005_historicalitem_is_special_and_more.py b/invoices/migrations/0005_historicalitem_is_special_and_more.py deleted file mode 100644 index 7524867..0000000 --- a/invoices/migrations/0005_historicalitem_is_special_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-22 08:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0004_historicalpayment_direction_payment_direction'), - ] - - operations = [ - migrations.AddField( - model_name='historicalitem', - name='is_special', - field=models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی'), - ), - migrations.AddField( - model_name='historicalitem', - name='special_kind', - field=models.CharField(blank=True, choices=[('repair', 'تعمیر'), ('replace', 'تعویض')], max_length=10, verbose_name='نوع ویژه'), - ), - migrations.AddField( - model_name='item', - name='is_special', - field=models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی'), - ), - migrations.AddField( - model_name='item', - name='special_kind', - field=models.CharField(blank=True, choices=[('repair', 'تعمیر'), ('replace', 'تعویض')], max_length=10, verbose_name='نوع ویژه'), - ), - ] diff --git a/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py b/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py deleted file mode 100644 index 5abdd01..0000000 --- a/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-22 08:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0005_historicalitem_is_special_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='historicalitem', - name='special_kind', - ), - migrations.RemoveField( - model_name='item', - name='special_kind', - ), - ] diff --git a/locations/migrations/0001_initial.py b/locations/migrations/0001_initial.py index def46a0..db3cdac 100644 --- a/locations/migrations/0001_initial.py +++ b/locations/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 from django.db import migrations, models diff --git a/processes/admin.py b/processes/admin.py index c8ad3ac..0cbe11f 100644 --- a/processes/admin.py +++ b/processes/admin.py @@ -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): 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