Add qoute step.
This commit is contained in:
		
							parent
							
								
									b71ea45681
								
							
						
					
					
						commit
						6ff4740d04
					
				
					 30 changed files with 3362 additions and 376 deletions
				
			
		| 
						 | 
					@ -24,6 +24,7 @@ urlpatterns = [
 | 
				
			||||||
    path('', include('accounts.urls')),
 | 
					    path('', include('accounts.urls')),
 | 
				
			||||||
    path('wells/', include('wells.urls')),
 | 
					    path('wells/', include('wells.urls')),
 | 
				
			||||||
    path('processes/', include('processes.urls')),
 | 
					    path('processes/', include('processes.urls')),
 | 
				
			||||||
 | 
					    path('invoices/', include('invoices.urls')),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if settings.DEBUG:
 | 
					if settings.DEBUG:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
					# Generated by Django 5.2.4 on 2025-08-14 09:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.core.validators
 | 
					import django.core.validators
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					import simple_history.models
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,10 +12,47 @@ class Migration(migrations.Migration):
 | 
				
			||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('locations', '0001_initial'),
 | 
				
			||||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='HistoricalProfile',
 | 
				
			||||||
 | 
					            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='تاریخ حذف')),
 | 
				
			||||||
 | 
					                ('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
 | 
				
			||||||
 | 
					                ('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='شماره حساب')),
 | 
				
			||||||
 | 
					                ('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='تصویر')),
 | 
				
			||||||
 | 
					                ('is_completed', models.BooleanField(default=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)),
 | 
				
			||||||
 | 
					                ('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
 | 
				
			||||||
 | 
					                ('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
 | 
				
			||||||
 | 
					                ('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
 | 
				
			||||||
 | 
					                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
				
			||||||
 | 
					                ('owner', 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='ایجاد کننده')),
 | 
				
			||||||
 | 
					                ('user', 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='کاربر')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            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(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name='Role',
 | 
					            name='Role',
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
| 
						 | 
					@ -48,8 +86,11 @@ class Migration(migrations.Migration):
 | 
				
			||||||
                ('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='شماره حساب')),
 | 
					                ('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='شماره حساب')),
 | 
				
			||||||
                ('phone_number_1', models.CharField(blank=True, max_length=11, 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='شماره تماس ۲')),
 | 
					                ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
 | 
				
			||||||
                ('pic', models.ImageField(default='../static/dist/img/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
 | 
					                ('pic', models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
 | 
				
			||||||
                ('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
 | 
					                ('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
 | 
				
			||||||
 | 
					                ('affairs', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور')),
 | 
				
			||||||
 | 
					                ('broker', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار')),
 | 
				
			||||||
 | 
					                ('county', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان')),
 | 
				
			||||||
                ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_profiles', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
					                ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_profiles', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
 | 
				
			||||||
                ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
 | 
					                ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
 | 
				
			||||||
                ('roles', models.ManyToManyField(blank=True, related_name='profiles', to='accounts.role', verbose_name='نقش\u200cها')),
 | 
					                ('roles', models.ManyToManyField(blank=True, related_name='profiles', to='accounts.role', verbose_name='نقش\u200cها')),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,75 +0,0 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 14:29
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.core.validators
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
import simple_history.models
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('accounts', '0001_initial'),
 | 
					 | 
				
			||||||
        ('locations', '0001_initial'),
 | 
					 | 
				
			||||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='profile',
 | 
					 | 
				
			||||||
            name='affairs',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='profile',
 | 
					 | 
				
			||||||
            name='broker',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='profile',
 | 
					 | 
				
			||||||
            name='county',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='profile',
 | 
					 | 
				
			||||||
            name='pic',
 | 
					 | 
				
			||||||
            field=models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='HistoricalProfile',
 | 
					 | 
				
			||||||
            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='تاریخ حذف')),
 | 
					 | 
				
			||||||
                ('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
 | 
					 | 
				
			||||||
                ('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='شماره حساب')),
 | 
					 | 
				
			||||||
                ('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='تصویر')),
 | 
					 | 
				
			||||||
                ('is_completed', models.BooleanField(default=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)),
 | 
					 | 
				
			||||||
                ('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
 | 
					 | 
				
			||||||
                ('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
 | 
					 | 
				
			||||||
                ('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
 | 
					 | 
				
			||||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
					 | 
				
			||||||
                ('owner', 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='ایجاد کننده')),
 | 
					 | 
				
			||||||
                ('user', 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='کاربر')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            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),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
					# Generated by Django 5.2.4 on 2025-08-14 09:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
import simple_history.models
 | 
					import simple_history.models
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					# 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='تصویر فیش'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@ from common.models import NameSlugModel, BaseModel
 | 
				
			||||||
from simple_history.models import HistoricalRecords
 | 
					from simple_history.models import HistoricalRecords
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from decimal import Decimal
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from django.core.validators import MinValueValidator
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
User = get_user_model()
 | 
					User = get_user_model()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,6 +126,21 @@ class Quote(NameSlugModel):
 | 
				
			||||||
        color = status_colors.get(self.status, 'secondary')
 | 
					        color = status_colors.get(self.status, 'secondary')
 | 
				
			||||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
					        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_paid_amount(self):
 | 
				
			||||||
 | 
					        """مبلغ پرداخت شده برای این پیشفاکتور بر اساس پرداختهای فاکتور مرتبط"""
 | 
				
			||||||
 | 
					        invoice = Invoice.objects.filter(quote=self).first()
 | 
				
			||||||
 | 
					        if not invoice:
 | 
				
			||||||
 | 
					            return Decimal('0')
 | 
				
			||||||
 | 
					        return sum(p.amount for p in invoice.payments.filter(is_deleted=False).all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_remaining_amount(self):
 | 
				
			||||||
 | 
					        """مبلغ باقیمانده بر اساس پرداختها"""
 | 
				
			||||||
 | 
					        paid = self.get_paid_amount()
 | 
				
			||||||
 | 
					        remaining = self.final_amount - paid
 | 
				
			||||||
 | 
					        if remaining < 0:
 | 
				
			||||||
 | 
					            remaining = Decimal('0')
 | 
				
			||||||
 | 
					        return remaining
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class QuoteItem(BaseModel):
 | 
					class QuoteItem(BaseModel):
 | 
				
			||||||
    """مدل آیتمهای پیشفاکتور"""
 | 
					    """مدل آیتمهای پیشفاکتور"""
 | 
				
			||||||
    quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیشفاکتور")
 | 
					    quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیشفاکتور")
 | 
				
			||||||
| 
						 | 
					@ -311,6 +329,7 @@ class Payment(BaseModel):
 | 
				
			||||||
    reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True)
 | 
					    reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True)
 | 
				
			||||||
    payment_date = models.DateField(verbose_name="تاریخ پرداخت")
 | 
					    payment_date = models.DateField(verbose_name="تاریخ پرداخت")
 | 
				
			||||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
					    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
				
			||||||
 | 
					    receipt_image = models.ImageField(upload_to='payments/%Y/%m/%d/', null=True, blank=True, verbose_name="تصویر فیش")
 | 
				
			||||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ثبت کننده")
 | 
					    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ثبت کننده")
 | 
				
			||||||
    history = HistoricalRecords()
 | 
					    history = HistoricalRecords()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										402
									
								
								invoices/templates/invoices/quote_payment_step.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										402
									
								
								invoices/templates/invoices/quote_payment_step.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,402 @@
 | 
				
			||||||
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load processes_tags %}
 | 
				
			||||||
 | 
					{% load humanize %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block sidebar %}
 | 
				
			||||||
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock sidebar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block navbar %}
 | 
				
			||||||
 | 
					    {% include 'navbars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock navbar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{{ step.name }} - درخواست {{ instance.code }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block style %}
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
 | 
				
			||||||
 | 
					<!-- Persian Date Picker CSS -->
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="https://unpkg.com/persian-datepicker@latest/dist/css/persian-datepicker.min.css">
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					@media print {
 | 
				
			||||||
 | 
					  .no-print { display: none !important; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% include '_toasts.html' %}
 | 
				
			||||||
 | 
					{% csrf_token %}
 | 
				
			||||||
 | 
					<div class="container-xxl flex-grow-1 container-p-y">
 | 
				
			||||||
 | 
					  <div class="row">
 | 
				
			||||||
 | 
					    <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					      <div class="d-flex align-items-center justify-content-between mb-3 no-print">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
 | 
				
			||||||
 | 
					          <small class="text-muted d-block">
 | 
				
			||||||
 | 
					            اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
 | 
				
			||||||
 | 
					            | نماینده: {{ instance.representative.profile.national_code|default:"-" }}
 | 
				
			||||||
 | 
					          </small>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					          <a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
				
			||||||
 | 
					            <i class="bx bx-printer"></i> پرینت
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="bs-stepper wizard-vertical vertical mt-2 no-print">
 | 
				
			||||||
 | 
					        {% stepper_header instance step %}
 | 
				
			||||||
 | 
					        <div class="bs-stepper-content">
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					          <form id="formAddPayment" enctype="multipart/form-data" onsubmit="return false;">
 | 
				
			||||||
 | 
					            {% csrf_token %}
 | 
				
			||||||
 | 
					            <div class="content active dstepper-block">
 | 
				
			||||||
 | 
					              <div class="content-header mb-3">
 | 
				
			||||||
 | 
					                <h6 class="mb-0">{{ step.name }}</h6>
 | 
				
			||||||
 | 
					                <small>ثبت فیشهای واریزی برای پیشفاکتور</small>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="row g-3">
 | 
				
			||||||
 | 
					                <div class="col-12 col-lg-5">
 | 
				
			||||||
 | 
					                  <div class="card h-100 border">
 | 
				
			||||||
 | 
					                    <div class="card-header">
 | 
				
			||||||
 | 
					                      <h5 class="card-title mb-0">ثبت فیش جدید</h5>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="card-body">
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">مبلغ (تومان)</label>
 | 
				
			||||||
 | 
					                        <input type="number" min="1" class="form-control" name="amount" id="id_amount" required>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">تاریخ پرداخت</label>
 | 
				
			||||||
 | 
					                        <input type="text" class="form-control" id="id_payment_date" name="payment_date" placeholder="انتخاب تاریخ" readonly required>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">روش پرداخت</label>
 | 
				
			||||||
 | 
					                        <select class="form-select" name="payment_method" id="id_payment_method" required>
 | 
				
			||||||
 | 
					                          <option value="bank_transfer">انتقال بانکی</option>
 | 
				
			||||||
 | 
					                          <option value="card">کارت بانکی</option>
 | 
				
			||||||
 | 
					                          <option value="cash">نقدی</option>
 | 
				
			||||||
 | 
					                          <option value="check">چک</option>
 | 
				
			||||||
 | 
					                          <option value="other">سایر</option>
 | 
				
			||||||
 | 
					                        </select>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">شماره مرجع</label>
 | 
				
			||||||
 | 
					                        <input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">تصویر فیش</label>
 | 
				
			||||||
 | 
					                        <input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="mb-3">
 | 
				
			||||||
 | 
					                        <label class="form-label">توضیحات</label>
 | 
				
			||||||
 | 
					                        <textarea class="form-control" rows="2" name="notes" id="id_notes"></textarea>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="d-flex justify-content-end">
 | 
				
			||||||
 | 
					                        <button type="button" id="btnAddPayment" class="btn btn-primary">افزودن فیش</button>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="col-12 col-lg-7">
 | 
				
			||||||
 | 
					                  <div class="card mb-3 border">
 | 
				
			||||||
 | 
					                    <div class="card-header d-flex justify-content-between">
 | 
				
			||||||
 | 
					                      <h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
 | 
				
			||||||
 | 
					                      <a href="{% url 'invoices:quote_preview_step' instance.id step.id|add:'-1' %}" class="btn btn-sm btn-label-secondary">مشاهده پیشفاکتور</a>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="card-body">
 | 
				
			||||||
 | 
					                      <div class="row g-3">
 | 
				
			||||||
 | 
					                        <div class="col-6">
 | 
				
			||||||
 | 
					                          <div class="border rounded p-3">
 | 
				
			||||||
 | 
					                            <div class="small text-muted">مبلغ نهایی پیشفاکتور</div>
 | 
				
			||||||
 | 
					                            <div class="h5 mt-1">{{ totals.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="col-6">
 | 
				
			||||||
 | 
					                          <div class="border rounded p-3">
 | 
				
			||||||
 | 
					                            <div class="small text-muted">مبلغ پرداختشده</div>
 | 
				
			||||||
 | 
					                            <div class="h5 mt-1 text-success">{{ totals.paid_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="col-6">
 | 
				
			||||||
 | 
					                          <div class="border rounded p-3">
 | 
				
			||||||
 | 
					                            <div class="small text-muted">مانده</div>
 | 
				
			||||||
 | 
					                            <div class="h5 mt-1 {% if totals.is_fully_paid %}text-success{% else %}text-danger{% endif %}">{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="col-6 d-flex align-items-center">
 | 
				
			||||||
 | 
					                          {% if totals.is_fully_paid %}
 | 
				
			||||||
 | 
					                            <span class="badge bg-success">تسویه کامل</span>
 | 
				
			||||||
 | 
					                          {% else %}
 | 
				
			||||||
 | 
					                            <span class="badge bg-warning text-dark">باقیمانده دارد</span>
 | 
				
			||||||
 | 
					                          {% endif %}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div class="card border">
 | 
				
			||||||
 | 
					                    <div class="card-header">
 | 
				
			||||||
 | 
					                      <h5 class="card-title mb-0">فیشهای ثبت شده</h5>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="table-responsive">
 | 
				
			||||||
 | 
					                      <table class="table table-striped mb-0">
 | 
				
			||||||
 | 
					                        <thead>
 | 
				
			||||||
 | 
					                          <tr>
 | 
				
			||||||
 | 
					                            <th>مبلغ</th>
 | 
				
			||||||
 | 
					                            <th>تاریخ</th>
 | 
				
			||||||
 | 
					                            <th>روش</th>
 | 
				
			||||||
 | 
					                            <th>شماره مرجع</th>
 | 
				
			||||||
 | 
					                            <th>تصویر</th>
 | 
				
			||||||
 | 
					                            <th style="width:120px">عملیات</th>
 | 
				
			||||||
 | 
					                          </tr>
 | 
				
			||||||
 | 
					                        </thead>
 | 
				
			||||||
 | 
					                        <tbody>
 | 
				
			||||||
 | 
					                          {% for p in payments %}
 | 
				
			||||||
 | 
					                          <tr>
 | 
				
			||||||
 | 
					                            <td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
 | 
				
			||||||
 | 
					                            <td>{{ p.payment_date|date:'Y/m/d' }}</td>
 | 
				
			||||||
 | 
					                            <td>{{ p.get_payment_method_display }}</td>
 | 
				
			||||||
 | 
					                            <td>{{ p.reference_number|default:'-' }}</td>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                              {% if p.receipt_image %}
 | 
				
			||||||
 | 
					                                <a href="{{ p.receipt_image.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="مشاهده" aria-label="مشاهده">
 | 
				
			||||||
 | 
					                                  <i class="bx bx-show"></i>
 | 
				
			||||||
 | 
					                                </a>
 | 
				
			||||||
 | 
					                              {% else %}
 | 
				
			||||||
 | 
					                                -
 | 
				
			||||||
 | 
					                              {% endif %}
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                              <div class="btn-group">
 | 
				
			||||||
 | 
					                                <button type="button" class="btn btn-sm btn-outline-primary" onclick="editPayment({{ p.id }})" title="ویرایش" aria-label="ویرایش">
 | 
				
			||||||
 | 
					                                  <i class="bx bx-edit"></i>
 | 
				
			||||||
 | 
					                                </button>
 | 
				
			||||||
 | 
					                                <button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal({{ p.id }})" title="حذف" aria-label="حذف">
 | 
				
			||||||
 | 
					                                  <i class="bx bx-trash"></i>
 | 
				
			||||||
 | 
					                                </button>
 | 
				
			||||||
 | 
					                              </div>
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
 | 
					                          </tr>
 | 
				
			||||||
 | 
					                          {% empty %}
 | 
				
			||||||
 | 
					                          <tr>
 | 
				
			||||||
 | 
					                            <td colspan="6" class="text-center text-muted">تا کنون فیشی ثبت نشده است</td>
 | 
				
			||||||
 | 
					                          </tr>
 | 
				
			||||||
 | 
					                          {% endfor %}
 | 
				
			||||||
 | 
					                        </tbody>
 | 
				
			||||||
 | 
					                      </table>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="col-12 d-flex justify-content-between mt-3">
 | 
				
			||||||
 | 
					                  {% if previous_step %}
 | 
				
			||||||
 | 
					                    <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">
 | 
				
			||||||
 | 
					                      <i class="bx bx-chevron-right bx-sm me-sm-2"></i>
 | 
				
			||||||
 | 
					                      قبلی
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                  {% else %}
 | 
				
			||||||
 | 
					                    <span></span>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  <button type="button" id="btnApprovePayments" class="btn btn-primary">  
 | 
				
			||||||
 | 
					                    تایید پرداختها
 | 
				
			||||||
 | 
					                    <i class="bx bx-chevron-left bx-sm ms-sm-2"></i>
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block script %}
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  const isFullyPaid = {{ totals.is_fully_paid|yesno:'true,false' }};
 | 
				
			||||||
 | 
					  function buildFormData(form) {
 | 
				
			||||||
 | 
					    const fd = new FormData(form);
 | 
				
			||||||
 | 
					    fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
 | 
				
			||||||
 | 
					    return fd;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  document.getElementById('btnAddPayment').addEventListener('click', function() {
 | 
				
			||||||
 | 
					    // Front-end validation
 | 
				
			||||||
 | 
					    const amount = document.getElementById('id_amount').value.trim();
 | 
				
			||||||
 | 
					    const payDate = document.getElementById('id_payment_date').value.trim();
 | 
				
			||||||
 | 
					    const method = document.getElementById('id_payment_method').value.trim();
 | 
				
			||||||
 | 
					    const ref = document.getElementById('id_reference_number').value.trim();
 | 
				
			||||||
 | 
					    const img = document.getElementById('id_receipt_image').files[0];
 | 
				
			||||||
 | 
					    const notes = document.getElementById('id_notes').value.trim();
 | 
				
			||||||
 | 
					    if (!amount || !payDate || !method || !ref || !img) {
 | 
				
			||||||
 | 
					      showToast('همه فیلدها الزامی است', 'danger');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const form = document.getElementById('formAddPayment');
 | 
				
			||||||
 | 
					    const fd = buildFormData(form);
 | 
				
			||||||
 | 
					    fetch('{% url "invoices:add_quote_payment" instance.id step.id %}', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      body: fd
 | 
				
			||||||
 | 
					    }).then(r => r.json()).then(resp => {
 | 
				
			||||||
 | 
					      if (resp.success) {
 | 
				
			||||||
 | 
					        showToast('فیش با موفقیت ثبت شد', 'success');
 | 
				
			||||||
 | 
					        if (resp.redirect) {
 | 
				
			||||||
 | 
					          setTimeout(() => { window.location.href = resp.redirect; }, 700);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast(resp.message || 'خطا در ثبت فیش', 'danger');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).catch(() => showToast('خطا در ارتباط با سرور', 'danger'));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let deleteTargetId = null;
 | 
				
			||||||
 | 
					  function openDeleteModal(id) {
 | 
				
			||||||
 | 
					    deleteTargetId = id;
 | 
				
			||||||
 | 
					    const modal = new bootstrap.Modal(document.getElementById('deletePaymentModal'));
 | 
				
			||||||
 | 
					    modal.show();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function confirmDeletePayment() {
 | 
				
			||||||
 | 
					    if (!deleteTargetId) return;
 | 
				
			||||||
 | 
					    const fd = new FormData();
 | 
				
			||||||
 | 
					    fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
 | 
				
			||||||
 | 
					    fetch(`{% url "invoices:delete_quote_payment" instance.id step.id 0 %}`.replace('/0/', `/${deleteTargetId}/`), {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      body: fd
 | 
				
			||||||
 | 
					    }).then(r => r.json()).then(resp => {
 | 
				
			||||||
 | 
					      if (resp.success && resp.redirect) {
 | 
				
			||||||
 | 
					        showToast('فیش با موفقیت حذف شد', 'success');
 | 
				
			||||||
 | 
					        setTimeout(() => { window.location.href = resp.redirect; }, 700);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast(resp.message || 'خطا در حذف فیش', 'danger');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).catch(() => showToast('خطا در ارتباط با سرور', 'danger'));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function editPayment(id) {
 | 
				
			||||||
 | 
					    // برای سادگی، همین فرم را استفاده نمیکنیم؛ میتوانید مدال ویرایش اضافه کنید
 | 
				
			||||||
 | 
					    alert('ویرایش فیش را بعدا با مدال تکمیل میکنیم. فعلا حذف و افزودن مجدد انجام دهید.');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function performApprovePayments() {
 | 
				
			||||||
 | 
					    const fd = new FormData();
 | 
				
			||||||
 | 
					    fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
 | 
				
			||||||
 | 
					    fetch('{% url "invoices:approve_payments" instance.id step.id %}', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      body: fd
 | 
				
			||||||
 | 
					    }).then(r => r.json()).then(resp => {
 | 
				
			||||||
 | 
					      if (resp.success && resp.redirect) {
 | 
				
			||||||
 | 
					        showToast(resp.message, 'success');
 | 
				
			||||||
 | 
					        setTimeout(() => { window.location.href = resp.redirect; }, 600);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showToast(resp.message || 'خطا در تایید پرداختها', 'danger');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).catch(() => showToast('خطا در ارتباط با سرور', 'danger'));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function openApproveModal() {
 | 
				
			||||||
 | 
					    const el = document.getElementById('approvePaymentsModal');
 | 
				
			||||||
 | 
					    const remEl = document.getElementById('remainingAmountText');
 | 
				
			||||||
 | 
					    if (remEl) {
 | 
				
			||||||
 | 
					      remEl.textContent = '{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Prefer jQuery plugin if available to avoid namespace issues
 | 
				
			||||||
 | 
					    if (window.$ && typeof $(el).modal === 'function') {
 | 
				
			||||||
 | 
					      $(el).modal('show');
 | 
				
			||||||
 | 
					    } else if (window.bootstrap && window.bootstrap.Modal) {
 | 
				
			||||||
 | 
					      const modal = new window.bootstrap.Modal(el);
 | 
				
			||||||
 | 
					      modal.show();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // fallback: force display
 | 
				
			||||||
 | 
					      el.classList.add('show');
 | 
				
			||||||
 | 
					      el.style.display = 'block';
 | 
				
			||||||
 | 
					      el.removeAttribute('aria-hidden');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  document.getElementById('btnApprovePayments').addEventListener('click', function() {
 | 
				
			||||||
 | 
					    if (isFullyPaid) {
 | 
				
			||||||
 | 
					      performApprovePayments();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      openApproveModal();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- Persian Date Picker JS -->
 | 
				
			||||||
 | 
					<script src="https://unpkg.com/persian-date@latest/dist/persian-date.min.js"></script>
 | 
				
			||||||
 | 
					<script src="https://unpkg.com/persian-datepicker@latest/dist/js/persian-datepicker.min.js"></script>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  (function initPersianDatePicker() {
 | 
				
			||||||
 | 
					    if (window.$ && $.fn.persianDatepicker && $('#id_payment_date').length) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        $('#id_payment_date').persianDatepicker({
 | 
				
			||||||
 | 
					          format: 'YYYY/MM/DD',
 | 
				
			||||||
 | 
					          initialValue: false,
 | 
				
			||||||
 | 
					          autoClose: true,
 | 
				
			||||||
 | 
					          persianDigit: false,
 | 
				
			||||||
 | 
					          observer: true,
 | 
				
			||||||
 | 
					          calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
 | 
				
			||||||
 | 
					          onSelect: function(unix) {
 | 
				
			||||||
 | 
					            const gregorianDate = new Date(unix);
 | 
				
			||||||
 | 
					            const year = gregorianDate.getFullYear();
 | 
				
			||||||
 | 
					            const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
 | 
				
			||||||
 | 
					            const day = String(gregorianDate.getDate()).padStart(2, '0');
 | 
				
			||||||
 | 
					            const gregorianDateString = `${year}-${month}-${day}`;
 | 
				
			||||||
 | 
					            if (window.persianDate) {
 | 
				
			||||||
 | 
					              const persianDate = new window.persianDate(unix);
 | 
				
			||||||
 | 
					              const persianDateString = persianDate.format('YYYY/MM/DD');
 | 
				
			||||||
 | 
					              $('#id_payment_date').val(persianDateString);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              $('#id_payment_date').val(gregorianDateString);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            $('#id_payment_date').attr('data-gregorian', gregorianDateString);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } catch (e) { console.error('Error initializing Persian Date Picker:', e); }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- Delete Confirmation Modal -->
 | 
				
			||||||
 | 
					<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
 | 
				
			||||||
 | 
					  <div class="modal-dialog">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <div class="modal-header">
 | 
				
			||||||
 | 
					        <h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف فیش</h5>
 | 
				
			||||||
 | 
					        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-body">
 | 
				
			||||||
 | 
					        آیا از حذف این فیش مطمئن هستید؟ این عمل قابل بازگشت نیست.
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-footer">
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- Approve Confirmation Modal (shown when remaining amount > 0) -->
 | 
				
			||||||
 | 
					<div class="modal fade" id="approvePaymentsModal" tabindex="-1" aria-labelledby="approvePaymentsModalLabel" aria-hidden="true">
 | 
				
			||||||
 | 
					  <div class="modal-dialog">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <div class="modal-header">
 | 
				
			||||||
 | 
					        <h5 class="modal-title" id="approvePaymentsModalLabel">تایید نهایی پرداختها</h5>
 | 
				
			||||||
 | 
					        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-body">
 | 
				
			||||||
 | 
					        مبلغی از پیشفاکتور هنوز پرداخت نشده است.
 | 
				
			||||||
 | 
					        <div class="mt-2">مانده: <strong id="remainingAmountText"></strong></div>
 | 
				
			||||||
 | 
					        آیا مطمئن هستید که میخواهید مرحله را تایید و به مرحله بعد بروید؟
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-footer">
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="performApprovePayments()">بله، تایید</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										287
									
								
								invoices/templates/invoices/quote_preview_step.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								invoices/templates/invoices/quote_preview_step.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,287 @@
 | 
				
			||||||
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load processes_tags %}
 | 
				
			||||||
 | 
					{% load humanize %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block sidebar %}
 | 
				
			||||||
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock sidebar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block navbar %}
 | 
				
			||||||
 | 
					    {% include 'navbars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock navbar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{{ step.name }} - درخواست {{ instance.code }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block style %}
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					@media print {
 | 
				
			||||||
 | 
					  .no-print { display: none !important; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% include '_toasts.html' %}
 | 
				
			||||||
 | 
					{% csrf_token %}
 | 
				
			||||||
 | 
					<div class="container-xxl flex-grow-1 container-p-y">
 | 
				
			||||||
 | 
					  <div class="row">
 | 
				
			||||||
 | 
					    <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					      <div class="d-flex align-items-center justify-content-between mb-3 no-print">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
 | 
				
			||||||
 | 
					          <small class="text-muted d-block">
 | 
				
			||||||
 | 
					            اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
 | 
				
			||||||
 | 
					            | نماینده: {{ instance.representative.profile.national_code|default:"-" }}
 | 
				
			||||||
 | 
					          </small>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					          <a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
				
			||||||
 | 
					            <i class="bx bx-printer"></i> پرینت
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="bs-stepper wizard-vertical vertical mt-2 no-print">
 | 
				
			||||||
 | 
					        {% stepper_header instance step %}
 | 
				
			||||||
 | 
					        <div class="bs-stepper-content">
 | 
				
			||||||
 | 
					            <!-- Invoice Preview Card -->
 | 
				
			||||||
 | 
					      <div class="card invoice-preview-card mt-4">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <div class="d-flex justify-content-between flex-xl-row flex-md-column flex-sm-row flex-column p-sm-3 p-0">
 | 
				
			||||||
 | 
					            <div class="mb-xl-0 mb-4">
 | 
				
			||||||
 | 
					              <div class="d-flex svg-illustration mb-3 gap-2">
 | 
				
			||||||
 | 
					                <span class="app-brand-logo demo">
 | 
				
			||||||
 | 
					                  <svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					                    <defs>
 | 
				
			||||||
 | 
					                      <path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
 | 
				
			||||||
 | 
					                      <path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
 | 
				
			||||||
 | 
					                      <path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
 | 
				
			||||||
 | 
					                      <path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
 | 
				
			||||||
 | 
					                    </defs>
 | 
				
			||||||
 | 
					                    <g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
 | 
				
			||||||
 | 
					                      <g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
 | 
				
			||||||
 | 
					                        <g id="Icon" transform="translate(27.000000, 15.000000)">
 | 
				
			||||||
 | 
					                          <g id="Mask" transform="translate(0.000000, 8.000000)">
 | 
				
			||||||
 | 
					                            <mask id="mask-2" fill="white">
 | 
				
			||||||
 | 
					                              <use xlink:href="#path-1"></use>
 | 
				
			||||||
 | 
					                            </mask>
 | 
				
			||||||
 | 
					                            <use fill="#696cff" xlink:href="#path-1"></use>
 | 
				
			||||||
 | 
					                            <g id="Path-3" mask="url(#mask-2)">
 | 
				
			||||||
 | 
					                              <use fill="#696cff" xlink:href="#path-3"></use>
 | 
				
			||||||
 | 
					                              <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
 | 
				
			||||||
 | 
					                            </g>
 | 
				
			||||||
 | 
					                            <g id="Path-4" mask="url(#mask-2)">
 | 
				
			||||||
 | 
					                              <use fill="#696cff" xlink:href="#path-4"></use>
 | 
				
			||||||
 | 
					                              <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
 | 
				
			||||||
 | 
					                            </g>
 | 
				
			||||||
 | 
					                          </g>
 | 
				
			||||||
 | 
					                          <g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
 | 
				
			||||||
 | 
					                            <use fill="#696cff" xlink:href="#path-5"></use>
 | 
				
			||||||
 | 
					                            <use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
 | 
				
			||||||
 | 
					                          </g>
 | 
				
			||||||
 | 
					                        </g>
 | 
				
			||||||
 | 
					                      </g>
 | 
				
			||||||
 | 
					                    </g>
 | 
				
			||||||
 | 
					                  </svg>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <span class="app-brand-text demo text-body fw-bold">شرکت آب منطقهای</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <p class="mb-1">دفتر مرکزی، خیابان اصلی</p>
 | 
				
			||||||
 | 
					              <p class="mb-1">تهران، ایران</p>
 | 
				
			||||||
 | 
					              <p class="mb-0">۰۲۱-۱۲۳۴۵۶۷۸</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <h4>پیشفاکتور #{{ quote.name }}</h4>
 | 
				
			||||||
 | 
					              <div class="mb-2">
 | 
				
			||||||
 | 
					                <span class="me-1">تاریخ صدور:</span>
 | 
				
			||||||
 | 
					                <span class="fw-medium">{{ quote.jcreated }}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div>
 | 
				
			||||||
 | 
					                <span class="me-1">معتبر تا:</span>
 | 
				
			||||||
 | 
					                <span class="fw-medium">{{ quote.valid_until|date:"Y/m/d" }}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <hr class="my-0">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <div class="row p-sm-3 p-0">
 | 
				
			||||||
 | 
					            <div class="col-xl-6 col-md-12 col-sm-5 col-12 mb-xl-0 mb-md-4 mb-sm-0 mb-4">
 | 
				
			||||||
 | 
					              <h6 class="pb-2">صادر شده برای:</h6>
 | 
				
			||||||
 | 
					              <p class="mb-1">{{ quote.customer.get_full_name }}</p>
 | 
				
			||||||
 | 
					              {% if instance.representative.profile %}
 | 
				
			||||||
 | 
					              <p class="mb-1">کد ملی: {{ instance.representative.profile.national_code }}</p>
 | 
				
			||||||
 | 
					              <p class="mb-1">{{ instance.representative.profile.address|default:"آدرس نامشخص" }}</p>
 | 
				
			||||||
 | 
					              <p class="mb-1">{{ instance.representative.profile.phone_number_1|default:"" }}</p>
 | 
				
			||||||
 | 
					              {% endif %}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-xl-6 col-md-12 col-sm-7 col-12">
 | 
				
			||||||
 | 
					              <h6 class="pb-2">اطلاعات چاه:</h6>
 | 
				
			||||||
 | 
					              <table>
 | 
				
			||||||
 | 
					                <tbody>
 | 
				
			||||||
 | 
					                  <tr>
 | 
				
			||||||
 | 
					                    <td class="pe-3">شماره اشتراک آب:</td>
 | 
				
			||||||
 | 
					                    <td>{{ instance.well.water_subscription_number }}</td>
 | 
				
			||||||
 | 
					                  </tr>
 | 
				
			||||||
 | 
					                  <tr>
 | 
				
			||||||
 | 
					                    <td class="pe-3">شماره اشتراک برق:</td>
 | 
				
			||||||
 | 
					                    <td>{{ instance.well.electricity_subscription_number|default:"-" }}</td>
 | 
				
			||||||
 | 
					                  </tr>
 | 
				
			||||||
 | 
					                  <tr>
 | 
				
			||||||
 | 
					                    <td class="pe-3">سریال کنتور:</td>
 | 
				
			||||||
 | 
					                    <td>{{ instance.well.water_meter_serial_number|default:"-" }}</td>
 | 
				
			||||||
 | 
					                  </tr>
 | 
				
			||||||
 | 
					                  <tr>
 | 
				
			||||||
 | 
					                    <td class="pe-3">قدرت چاه:</td>
 | 
				
			||||||
 | 
					                    <td>{{ instance.well.well_power|default:"-" }}</td>
 | 
				
			||||||
 | 
					                  </tr>
 | 
				
			||||||
 | 
					                  <tr>
 | 
				
			||||||
 | 
					                    <td class="pe-3">کد درخواست:</td>
 | 
				
			||||||
 | 
					                    <td>{{ instance.code }}</td>
 | 
				
			||||||
 | 
					                  </tr>
 | 
				
			||||||
 | 
					                </tbody>
 | 
				
			||||||
 | 
					              </table>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="table-responsive">
 | 
				
			||||||
 | 
					          <table class="table border-top m-0">
 | 
				
			||||||
 | 
					            <thead>
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <th>آیتم</th>
 | 
				
			||||||
 | 
					                <th>توضیحات</th>
 | 
				
			||||||
 | 
					                <th>قیمت واحد</th>
 | 
				
			||||||
 | 
					                <th>تعداد</th>
 | 
				
			||||||
 | 
					                <th>قیمت کل</th>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					            </thead>
 | 
				
			||||||
 | 
					            <tbody>
 | 
				
			||||||
 | 
					              {% for quote_item in quote.items.all %}
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td class="text-nowrap">{{ quote_item.item.name }}</td>
 | 
				
			||||||
 | 
					                <td class="text-nowrap">{{ quote_item.item.description|default:"-" }}</td>
 | 
				
			||||||
 | 
					                <td>{{ quote_item.unit_price|floatformat:0|intcomma:False }} تومان</td>
 | 
				
			||||||
 | 
					                <td>{{ quote_item.quantity }}</td>
 | 
				
			||||||
 | 
					                <td>{{ quote_item.total_price|floatformat:0|intcomma:False }} تومان</td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					              {% endfor %}
 | 
				
			||||||
 | 
					              <tr>
 | 
				
			||||||
 | 
					                <td colspan="3" class="align-top px-4 py-5">
 | 
				
			||||||
 | 
					                  <p class="mb-2">
 | 
				
			||||||
 | 
					                    <span class="me-1 fw-medium">صادر کننده:</span>
 | 
				
			||||||
 | 
					                    <span>{{ quote.created_by.get_full_name }}</span>
 | 
				
			||||||
 | 
					                  </p>
 | 
				
			||||||
 | 
					                  <span>با تشکر از انتخاب شما</span>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td class="text-end px-4 py-5">
 | 
				
			||||||
 | 
					                  <p class="mb-2">جمع کل:</p>
 | 
				
			||||||
 | 
					                  {% if quote.discount_amount > 0 %}
 | 
				
			||||||
 | 
					                  <p class="mb-2">تخفیف:</p>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  <p class="mb-0 fw-bold">مبلغ نهایی:</p>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td class="px-4 py-5">
 | 
				
			||||||
 | 
					                  <p class="fw-medium mb-2">{{ quote.total_amount|floatformat:0|intcomma:False }} تومان</p>
 | 
				
			||||||
 | 
					                  {% if quote.discount_amount > 0 %}
 | 
				
			||||||
 | 
					                  <p class="fw-medium mb-2">{{ quote.discount_amount|floatformat:0|intcomma:False }} تومان</p>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  <p class="fw-bold mb-0">{{ quote.final_amount|floatformat:0|intcomma:False }} تومان</p>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					          </table>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% if quote.notes %}
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					          <div class="row">
 | 
				
			||||||
 | 
					            <div class="col-12">
 | 
				
			||||||
 | 
					              <span class="fw-medium">یادداشت:</span>
 | 
				
			||||||
 | 
					              <span>{{ quote.notes }}</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- Action Buttons -->
 | 
				
			||||||
 | 
					      <div class="row mt-4 no-print">
 | 
				
			||||||
 | 
					        <div class="col-12">
 | 
				
			||||||
 | 
					          <div class="d-flex justify-content-between">
 | 
				
			||||||
 | 
					            {% if previous_step %}
 | 
				
			||||||
 | 
					              <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" 
 | 
				
			||||||
 | 
					                 class="btn btn-label-secondary">
 | 
				
			||||||
 | 
					                <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
				
			||||||
 | 
					                <span class="align-middle d-sm-inline-block d-none">ویرایش اقلام</span>
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            {% else %}
 | 
				
			||||||
 | 
					              <span></span>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            {% if step_instance.status == 'completed' %}
 | 
				
			||||||
 | 
					              {% if next_step %}
 | 
				
			||||||
 | 
					                <a href="{% url 'processes:step_detail' instance.id next_step.id %}" 
 | 
				
			||||||
 | 
					                   class="btn btn-primary">
 | 
				
			||||||
 | 
					                  <span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
 | 
				
			||||||
 | 
					                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              {% else %}
 | 
				
			||||||
 | 
					                <button class="btn btn-success" type="button">اتمام</button>
 | 
				
			||||||
 | 
					              {% endif %}
 | 
				
			||||||
 | 
					            {% else %}
 | 
				
			||||||
 | 
					              <button type="button" class="btn btn-primary" id="btnApproveQuote">
 | 
				
			||||||
 | 
					                تایید پیشفاکتور
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block script %}
 | 
				
			||||||
 | 
					<script src="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.js' %}"></script>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  document.addEventListener('DOMContentLoaded', function () {
 | 
				
			||||||
 | 
					    // Quote approval
 | 
				
			||||||
 | 
					    const btnApproveQuote = document.getElementById('btnApproveQuote');
 | 
				
			||||||
 | 
					    if (btnApproveQuote) {
 | 
				
			||||||
 | 
					      btnApproveQuote.addEventListener('click', function() {
 | 
				
			||||||
 | 
					        btnApproveQuote.disabled = true;
 | 
				
			||||||
 | 
					        fetch('{% url "invoices:approve_quote" instance.id step.id %}', {
 | 
				
			||||||
 | 
					          method: 'POST',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
 | 
				
			||||||
 | 
					            'Content-Type': 'application/x-www-form-urlencoded',
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }).then(r => r.json()).then(resp => {
 | 
				
			||||||
 | 
					          if (resp.success) {
 | 
				
			||||||
 | 
					            showToast('پیشفاکتور با موفقیت تایید شد', 'success');
 | 
				
			||||||
 | 
					            if (resp.redirect) {
 | 
				
			||||||
 | 
					              window.location.href = resp.redirect;
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            setTimeout(() => { window.location.reload(); }, 800);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            showToast(resp.message || 'خطا در تایید پیشفاکتور', 'danger');
 | 
				
			||||||
 | 
					            btnApproveQuote.disabled = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }).catch(() => {
 | 
				
			||||||
 | 
					          showToast('خطا در ارتباط با سرور', 'danger');
 | 
				
			||||||
 | 
					          btnApproveQuote.disabled = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										283
									
								
								invoices/templates/invoices/quote_print.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								invoices/templates/invoices/quote_print.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,283 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="fa" dir="rtl">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <title>پیشفاکتور {{ quote.name }} - {{ instance.code }}</title>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <!-- Bootstrap CSS -->
 | 
				
			||||||
 | 
					    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        @page {
 | 
				
			||||||
 | 
					            size: A4;
 | 
				
			||||||
 | 
					            margin: 1cm;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        body {
 | 
				
			||||||
 | 
					            font-family: 'Vazirmatn', sans-serif;
 | 
				
			||||||
 | 
					            font-size: 14px;
 | 
				
			||||||
 | 
					            line-height: 1.6;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @media print {
 | 
				
			||||||
 | 
					            body { print-color-adjust: exact; }
 | 
				
			||||||
 | 
					            .page-break { page-break-before: always; }
 | 
				
			||||||
 | 
					            .no-print { display: none !important; }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .invoice-header {
 | 
				
			||||||
 | 
					            border-bottom: 2px solid #696cff;
 | 
				
			||||||
 | 
					            padding-bottom: 20px;
 | 
				
			||||||
 | 
					            margin-bottom: 30px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .company-logo {
 | 
				
			||||||
 | 
					            font-size: 24px;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					            color: #696cff;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .invoice-title {
 | 
				
			||||||
 | 
					            font-size: 28px;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					            color: #333;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .info-table td {
 | 
				
			||||||
 | 
					            padding: 5px 10px;
 | 
				
			||||||
 | 
					            border: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .items-table {
 | 
				
			||||||
 | 
					            border: 1px solid #dee2e6;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .items-table th {
 | 
				
			||||||
 | 
					            background-color: #f8f9fa;
 | 
				
			||||||
 | 
					            border-bottom: 2px solid #dee2e6;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .items-table td {
 | 
				
			||||||
 | 
					            border-bottom: 1px solid #dee2e6;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .total-section {
 | 
				
			||||||
 | 
					            background-color: #f8f9fa;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .signature-section {
 | 
				
			||||||
 | 
					            margin-top: 50px;
 | 
				
			||||||
 | 
					            border-top: 1px solid #dee2e6;
 | 
				
			||||||
 | 
					            padding-top: 30px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        .signature-box {
 | 
				
			||||||
 | 
					            border: 1px dashed #ccc;
 | 
				
			||||||
 | 
					            height: 80px;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
 | 
					            align-items: center;
 | 
				
			||||||
 | 
					            justify-content: center;
 | 
				
			||||||
 | 
					            color: #666;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <div class="container-fluid">
 | 
				
			||||||
 | 
					        <!-- Print Button (hidden in print) -->
 | 
				
			||||||
 | 
					        <div class="no-print mb-3">
 | 
				
			||||||
 | 
					            <button onclick="window.print()" class="btn btn-primary">
 | 
				
			||||||
 | 
					                <i class="bi bi-printer"></i> پرینت
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					            <button onclick="window.close()" class="btn btn-secondary">
 | 
				
			||||||
 | 
					                بستن
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Invoice Header -->
 | 
				
			||||||
 | 
					        <div class="invoice-header">
 | 
				
			||||||
 | 
					            <div class="row">
 | 
				
			||||||
 | 
					                <div class="col-6">
 | 
				
			||||||
 | 
					                    <div class="company-logo mb-3">
 | 
				
			||||||
 | 
					                        شرکت آب منطقهای
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="company-info">
 | 
				
			||||||
 | 
					                        <p class="mb-1">دفتر مرکزی، خیابان اصلی</p>
 | 
				
			||||||
 | 
					                        <p class="mb-1">تهران، ایران</p>
 | 
				
			||||||
 | 
					                        <p class="mb-1">تلفن: ۰۲۱-۱۲۳۴۵۶۷۸</p>
 | 
				
			||||||
 | 
					                        <p class="mb-0">ایمیل: info@watercompany.ir</p>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="col-6 text-end">
 | 
				
			||||||
 | 
					                    <div class="invoice-title">پیشفاکتور</div>
 | 
				
			||||||
 | 
					                    <div class="mt-3">
 | 
				
			||||||
 | 
					                        <table class="info-table">
 | 
				
			||||||
 | 
					                            <tr>
 | 
				
			||||||
 | 
					                                <td><strong>شماره پیشفاکتور:</strong></td>
 | 
				
			||||||
 | 
					                                <td>{{ quote.name }}</td>
 | 
				
			||||||
 | 
					                            </tr>
 | 
				
			||||||
 | 
					                            <tr>
 | 
				
			||||||
 | 
					                                <td><strong>کد درخواست:</strong></td>
 | 
				
			||||||
 | 
					                                <td>{{ instance.code }}</td>
 | 
				
			||||||
 | 
					                            </tr>
 | 
				
			||||||
 | 
					                            <tr>
 | 
				
			||||||
 | 
					                                <td><strong>تاریخ صدور:</strong></td>
 | 
				
			||||||
 | 
					                                <td>{{ quote.created|date:"Y/m/d" }}</td>
 | 
				
			||||||
 | 
					                            </tr>
 | 
				
			||||||
 | 
					                            <tr>
 | 
				
			||||||
 | 
					                                <td><strong>معتبر تا:</strong></td>
 | 
				
			||||||
 | 
					                                <td>{{ quote.valid_until|date:"Y/m/d" }}</td>
 | 
				
			||||||
 | 
					                            </tr>
 | 
				
			||||||
 | 
					                        </table>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Customer & Well Info -->
 | 
				
			||||||
 | 
					        <div class="row mb-4">
 | 
				
			||||||
 | 
					            <div class="col-6">
 | 
				
			||||||
 | 
					                <h6 class="fw-bold mb-3">مشخصات مشترک:</h6>
 | 
				
			||||||
 | 
					                <table class="info-table">
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>نام و نام خانوادگی:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ quote.customer.get_full_name }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    {% if instance.representative.profile %}
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>کد ملی:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.representative.profile.national_code }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>تلفن:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.representative.profile.phone_number_1|default:"-" }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>آدرس:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.representative.profile.address|default:"آدرس نامشخص" }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                </table>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-6">
 | 
				
			||||||
 | 
					                <h6 class="fw-bold mb-3">مشخصات چاه:</h6>
 | 
				
			||||||
 | 
					                <table class="info-table">
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>شماره اشتراک آب:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.well.water_subscription_number }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>شماره اشتراک برق:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.well.electricity_subscription_number|default:"-" }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>سریال کنتور:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.well.water_meter_serial_number|default:"-" }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td><strong>قدرت چاه:</strong></td>
 | 
				
			||||||
 | 
					                        <td>{{ instance.well.well_power|default:"-" }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                </table>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Items Table -->
 | 
				
			||||||
 | 
					        <div class="mb-4">
 | 
				
			||||||
 | 
					            <table class="table items-table">
 | 
				
			||||||
 | 
					                <thead>
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <th style="width: 5%">ردیف</th>
 | 
				
			||||||
 | 
					                        <th style="width: 30%">شرح کالا/خدمات</th>
 | 
				
			||||||
 | 
					                        <th style="width: 30%">توضیحات</th>
 | 
				
			||||||
 | 
					                        <th style="width: 10%">تعداد</th>
 | 
				
			||||||
 | 
					                        <th style="width: 12.5%">قیمت واحد (تومان)</th>
 | 
				
			||||||
 | 
					                        <th style="width: 12.5%">قیمت کل (تومان)</th>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                </thead>
 | 
				
			||||||
 | 
					                <tbody>
 | 
				
			||||||
 | 
					                    {% for quote_item in quote.items.all %}
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td>{{ forloop.counter }}</td>
 | 
				
			||||||
 | 
					                        <td class="text-start">{{ quote_item.item.name }}</td>
 | 
				
			||||||
 | 
					                        <td class="text-start">{{ quote_item.item.description|default:"-" }}</td>
 | 
				
			||||||
 | 
					                        <td>{{ quote_item.quantity }}</td>
 | 
				
			||||||
 | 
					                        <td>{{ quote_item.unit_price|floatformat:0 }}</td>
 | 
				
			||||||
 | 
					                        <td>{{ quote_item.total_price|floatformat:0 }}</td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    {% endfor %}
 | 
				
			||||||
 | 
					                </tbody>
 | 
				
			||||||
 | 
					                <tfoot>
 | 
				
			||||||
 | 
					                    <tr class="total-section">
 | 
				
			||||||
 | 
					                        <td colspan="5" class="text-end"><strong>جمع کل:</strong></td>
 | 
				
			||||||
 | 
					                        <td><strong>{{ quote.total_amount|floatformat:0 }} تومان</strong></td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    {% if quote.discount_amount > 0 %}
 | 
				
			||||||
 | 
					                    <tr class="total-section">
 | 
				
			||||||
 | 
					                        <td colspan="5" class="text-end"><strong>تخفیف:</strong></td>
 | 
				
			||||||
 | 
					                        <td><strong>{{ quote.discount_amount|floatformat:0 }} تومان</strong></td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                    <tr class="total-section border-top border-2">
 | 
				
			||||||
 | 
					                        <td colspan="5" class="text-end"><strong>مبلغ نهایی:</strong></td>
 | 
				
			||||||
 | 
					                        <td><strong>{{ quote.final_amount|floatformat:0 }} تومان</strong></td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                </tfoot>
 | 
				
			||||||
 | 
					            </table>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Notes -->
 | 
				
			||||||
 | 
					        {% if quote.notes %}
 | 
				
			||||||
 | 
					        <div class="mb-4">
 | 
				
			||||||
 | 
					            <h6 class="fw-bold">یادداشت:</h6>
 | 
				
			||||||
 | 
					            <p>{{ quote.notes }}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Additional Info -->
 | 
				
			||||||
 | 
					        <div class="mb-4">
 | 
				
			||||||
 | 
					            <p><strong>صادر کننده:</strong> {{ quote.created_by.get_full_name }}</p>
 | 
				
			||||||
 | 
					            <p class="text-muted">این پیشفاکتور تا تاریخ {{ quote.valid_until|date:"Y/m/d" }} معتبر است.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Signature Section -->
 | 
				
			||||||
 | 
					        <div class="signature-section">
 | 
				
			||||||
 | 
					            <div class="row">
 | 
				
			||||||
 | 
					                <div class="col-6">
 | 
				
			||||||
 | 
					                    <div class="text-center">
 | 
				
			||||||
 | 
					                        <p class="mb-2"><strong>امضای مشترک</strong></p>
 | 
				
			||||||
 | 
					                        <div class="signature-box">
 | 
				
			||||||
 | 
					                            امضا و مهر
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <p class="mt-2 small">تاریخ: ____/____/____</p>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="col-6">
 | 
				
			||||||
 | 
					                    <div class="text-center">
 | 
				
			||||||
 | 
					                        <p class="mb-2"><strong>امضای شرکت</strong></p>
 | 
				
			||||||
 | 
					                        <div class="signature-box">
 | 
				
			||||||
 | 
					                            امضا و مهر
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <p class="mt-2 small">تاریخ: ____/____/____</p>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- Footer -->
 | 
				
			||||||
 | 
					        <div class="text-center mt-4 small text-muted">
 | 
				
			||||||
 | 
					            این پیشفاکتور توسط سیستم مدیریت فرآیندها تولید شده است.
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					        // Auto print on load (optional)
 | 
				
			||||||
 | 
					        // window.onload = function() { window.print(); }
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										204
									
								
								invoices/templates/invoices/quote_step.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								invoices/templates/invoices/quote_step.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,204 @@
 | 
				
			||||||
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load processes_tags %}
 | 
				
			||||||
 | 
					{% load humanize %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block sidebar %}
 | 
				
			||||||
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock sidebar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block navbar %}
 | 
				
			||||||
 | 
					    {% include 'navbars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock navbar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{{ step.name }} - درخواست {{ instance.code }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block style %}
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% include '_toasts.html' %}
 | 
				
			||||||
 | 
					<div class="container-xxl flex-grow-1 container-p-y">
 | 
				
			||||||
 | 
					  <div class="row">
 | 
				
			||||||
 | 
					    <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					      <div class="d-flex align-items-center justify-content-between mb-3">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
 | 
				
			||||||
 | 
					          <small class="text-muted d-block">
 | 
				
			||||||
 | 
					            اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
 | 
				
			||||||
 | 
					            | نماینده: {{ instance.representative.profile.national_code|default:"-" }}
 | 
				
			||||||
 | 
					          </small>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
				
			||||||
 | 
					        {% stepper_header instance step %}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="bs-stepper-content">
 | 
				
			||||||
 | 
					          <form>
 | 
				
			||||||
 | 
					            {% csrf_token %}
 | 
				
			||||||
 | 
					            <div class="content active dstepper-block">
 | 
				
			||||||
 | 
					              <div class="content-header mb-3">
 | 
				
			||||||
 | 
					                <h6 class="mb-0">{{ step.name }}</h6>
 | 
				
			||||||
 | 
					                <small>{{ step.description|default:' ' }}</small>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="row g-3">
 | 
				
			||||||
 | 
					                {% if existing_quote %}
 | 
				
			||||||
 | 
					                <div class="col-12 mb-3">
 | 
				
			||||||
 | 
					                  <div class="alert alert-info">
 | 
				
			||||||
 | 
					                    <h6>پیشفاکتور موجود</h6>
 | 
				
			||||||
 | 
					                    <span class="mb-1">نام: {{ existing_quote.name }} | </span>
 | 
				
			||||||
 | 
					                    <span class="mb-1">مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
 | 
				
			||||||
 | 
					                    <span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="col-12">
 | 
				
			||||||
 | 
					                  <div class="table-responsive">
 | 
				
			||||||
 | 
					                    <table class="table table-sm align-middle">
 | 
				
			||||||
 | 
					                      <thead>
 | 
				
			||||||
 | 
					                        <tr>
 | 
				
			||||||
 | 
					                          <th style="width:40px"></th>
 | 
				
			||||||
 | 
					                          <th>آیتم</th>
 | 
				
			||||||
 | 
					                          <th>قیمت واحد</th>
 | 
				
			||||||
 | 
					                          <th style="width:140px">تعداد</th>
 | 
				
			||||||
 | 
					                        </tr>
 | 
				
			||||||
 | 
					                      </thead>
 | 
				
			||||||
 | 
					                      <tbody>
 | 
				
			||||||
 | 
					                        {% for item in items %}
 | 
				
			||||||
 | 
					                        {% with selected_qty=existing_quote_items|get_item:item.id %}
 | 
				
			||||||
 | 
					                        <tr>
 | 
				
			||||||
 | 
					                          <td>
 | 
				
			||||||
 | 
					                            <input class="form-check-input quote-item-check" type="checkbox"
 | 
				
			||||||
 | 
					                                   data-item-id="{{ item.id }}"
 | 
				
			||||||
 | 
					                                   data-is-default="{% if item.is_default_in_quotes %}1{% else %}0{% endif %}"
 | 
				
			||||||
 | 
					                                   {% if selected_qty %}checked{% elif item.is_default_in_quotes %}checked{% endif %}
 | 
				
			||||||
 | 
					                                   {% if item.is_default_in_quotes %}disabled title="آیتم پیشفرض است و قابل حذف نیست"{% endif %}>
 | 
				
			||||||
 | 
					                          </td>
 | 
				
			||||||
 | 
					                          <td>
 | 
				
			||||||
 | 
					                            <div class="d-flex flex-column">
 | 
				
			||||||
 | 
					                              <span class="fw-semibold">{{ item.name }}
 | 
				
			||||||
 | 
					                                {% if item.is_default_in_quotes %}
 | 
				
			||||||
 | 
					                                <span class="badge bg-label-primary me-2">پیشفرض</span>
 | 
				
			||||||
 | 
					                              {% endif %}
 | 
				
			||||||
 | 
					                              </span>
 | 
				
			||||||
 | 
					                              
 | 
				
			||||||
 | 
					                              {% if item.description %}<small class="text-muted">{{ item.description }}</small>{% endif %}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                          </td>
 | 
				
			||||||
 | 
					                          <td>{{ item.unit_price|floatformat:0|intcomma:False }} تومان</td>
 | 
				
			||||||
 | 
					                          <td>
 | 
				
			||||||
 | 
					                            <input type="number" class="form-control form-control-sm quote-item-qty" min="1" 
 | 
				
			||||||
 | 
					                                   data-item-id="{{ item.id }}" 
 | 
				
			||||||
 | 
					                                   value="{% if selected_qty %}{{ selected_qty }}{% else %}{{ item.default_quantity }}{% endif %}">
 | 
				
			||||||
 | 
					                          </td>
 | 
				
			||||||
 | 
					                        </tr>
 | 
				
			||||||
 | 
					                        {% endwith %}
 | 
				
			||||||
 | 
					                        {% endfor %}
 | 
				
			||||||
 | 
					                      </tbody>
 | 
				
			||||||
 | 
					                    </table>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="col-12 d-flex justify-content-between">
 | 
				
			||||||
 | 
					                  {% if previous_step %}
 | 
				
			||||||
 | 
					                    <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" 
 | 
				
			||||||
 | 
					                       class="btn btn-label-secondary">
 | 
				
			||||||
 | 
					                      <i class="bx bx-chevron-left bx-sm ms-sm-n2"></i>
 | 
				
			||||||
 | 
					                      <span class="align-middle d-sm-inline-block d-none">قبلی</span>
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                  {% else %}
 | 
				
			||||||
 | 
					                    <span></span>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                  {% if step_instance.status == 'completed' %}
 | 
				
			||||||
 | 
					                    {% if next_step %}
 | 
				
			||||||
 | 
					                    <div class="d-flex justify-content-end mt-3">
 | 
				
			||||||
 | 
					                      <button type="button" class="btn btn-primary" id="btnCreateQuote">
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        {% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
 | 
				
			||||||
 | 
					                        و بعدی
 | 
				
			||||||
 | 
					                        <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
				
			||||||
 | 
					                      </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {% else %}
 | 
				
			||||||
 | 
					                      <button class="btn btn-success" type="button">اتمام</button>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  {% else %}
 | 
				
			||||||
 | 
					                  <button type="button" class="btn btn-primary" id="btnCreateQuote">
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                    {% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
 | 
				
			||||||
 | 
					                    و بعدی
 | 
				
			||||||
 | 
					                    <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block script %}
 | 
				
			||||||
 | 
					<script src="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.js' %}"></script>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  document.addEventListener('DOMContentLoaded', function () {
 | 
				
			||||||
 | 
					    // Quote creation
 | 
				
			||||||
 | 
					    const btnCreateQuote = document.getElementById('btnCreateQuote');
 | 
				
			||||||
 | 
					    if (btnCreateQuote) {
 | 
				
			||||||
 | 
					      btnCreateQuote.addEventListener('click', function() {
 | 
				
			||||||
 | 
					        const selections = [];
 | 
				
			||||||
 | 
					        document.querySelectorAll('.quote-item-check').forEach(chk => {
 | 
				
			||||||
 | 
					          if (chk.checked) {
 | 
				
			||||||
 | 
					            const id = chk.getAttribute('data-item-id');
 | 
				
			||||||
 | 
					            const isDefault = chk.getAttribute('data-is-default') === '1';
 | 
				
			||||||
 | 
					            const qtyInput = document.querySelector(`.quote-item-qty[data-item-id="${id}"]`);
 | 
				
			||||||
 | 
					            const qty = qtyInput ? parseInt(qtyInput.value || '1', 10) : 1;
 | 
				
			||||||
 | 
					            selections.push({ id, qty, isDefault });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (selections.length === 0) {
 | 
				
			||||||
 | 
					          showToast('حداقل یک آیتم را انتخاب کنید', 'danger');
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const payload = new FormData();
 | 
				
			||||||
 | 
					        payload.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
 | 
				
			||||||
 | 
					        payload.append('items', JSON.stringify(selections));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        btnCreateQuote.disabled = true;
 | 
				
			||||||
 | 
					        fetch('{% url "invoices:create_quote" instance.id step.id %}', {
 | 
				
			||||||
 | 
					          method: 'POST',
 | 
				
			||||||
 | 
					          body: payload
 | 
				
			||||||
 | 
					        }).then(r => r.json()).then(resp => {
 | 
				
			||||||
 | 
					          if (resp.success) {
 | 
				
			||||||
 | 
					            showToast('پیشفاکتور با موفقیت ثبت شد', 'success');
 | 
				
			||||||
 | 
					            if (resp.redirect) {
 | 
				
			||||||
 | 
					              window.location.href = resp.redirect;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              setTimeout(() => { window.location.reload(); }, 800);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            showToast(resp.message || 'خطا در ثبت پیشفاکتور', 'danger');
 | 
				
			||||||
 | 
					            btnCreateQuote.disabled = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }).catch(() => {
 | 
				
			||||||
 | 
					          showToast('خطا در ارتباط با سرور', 'danger');
 | 
				
			||||||
 | 
					          btnCreateQuote.disabled = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										24
									
								
								invoices/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								invoices/urls.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app_name = 'invoices'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					urlpatterns = [
 | 
				
			||||||
 | 
					    # Quote step for process instances
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/quote/', views.quote_step, name='quote_step'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/quote/create/', views.create_quote, name='create_quote'),
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Quote preview step (step 2)
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/quote-preview/', views.quote_preview_step, name='quote_preview_step'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/approve/', views.approve_quote, name='approve_quote'),
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Quote payments step (step 3)
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/payments/', views.quote_payment_step, name='quote_payment_step'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/payments/add/', views.add_quote_payment, name='add_quote_payment'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/payments/<int:payment_id>/update/', views.update_quote_payment, name='update_quote_payment'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/payments/<int:payment_id>/delete/', views.delete_quote_payment, name='delete_quote_payment'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/payments/approve/', views.approve_payments, name='approve_payments'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Quote print
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/quote/print/', views.quote_print, name='quote_print'),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,415 @@
 | 
				
			||||||
from django.shortcuts import render
 | 
					from django.shortcuts import render, get_object_or_404, redirect
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from django.contrib.auth.decorators import login_required
 | 
				
			||||||
 | 
					from django.contrib import messages
 | 
				
			||||||
 | 
					from django.http import JsonResponse
 | 
				
			||||||
 | 
					from django.views.decorators.http import require_POST
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from decimal import Decimal, InvalidOperation
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create your views here.
 | 
					from processes.models import ProcessInstance, ProcessStep, StepInstance
 | 
				
			||||||
 | 
					from .models import Item, Quote, QuoteItem, Payment, Invoice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def quote_step(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """مرحله انتخاب اقلام و ساخت پیشفاکتور"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(
 | 
				
			||||||
 | 
					        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
				
			||||||
 | 
					        id=instance_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # بررسی دسترسی به مرحله
 | 
				
			||||||
 | 
					    if not instance.can_access_step(step):
 | 
				
			||||||
 | 
					        messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
 | 
				
			||||||
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # دریافت آیتمها
 | 
				
			||||||
 | 
					    items = Item.objects.all().order_by('name')
 | 
				
			||||||
 | 
					    existing_quote = Quote.objects.filter(process_instance=instance).first()
 | 
				
			||||||
 | 
					    existing_quote_items = {}
 | 
				
			||||||
 | 
					    if existing_quote:
 | 
				
			||||||
 | 
					        existing_quote_items = {qi.item_id: qi.quantity for qi in existing_quote.items.all()}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    step_instance = instance.step_instances.filter(step=step).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Navigation logic
 | 
				
			||||||
 | 
					    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render(request, 'invoices/quote_step.html', {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'step': step,
 | 
				
			||||||
 | 
					        'step_instance': step_instance,
 | 
				
			||||||
 | 
					        'items': items,
 | 
				
			||||||
 | 
					        'existing_quote_items': existing_quote_items,
 | 
				
			||||||
 | 
					        'existing_quote': existing_quote,
 | 
				
			||||||
 | 
					        'previous_step': previous_step,
 | 
				
			||||||
 | 
					        'next_step': next_step,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def create_quote(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """ساخت/بروزرسانی پیشفاکتور از اقلام انتخابی"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        items_payload = json.loads(request.POST.get('items') or '[]')
 | 
				
			||||||
 | 
					    except json.JSONDecodeError:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'دادههای اقلام نامعتبر است'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # اطمینان از حضور اقلام پیشفرض حتی اگر کلاینت ارسال نکرده باشد
 | 
				
			||||||
 | 
					    payload_by_id = {}
 | 
				
			||||||
 | 
					    for entry in items_payload:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            iid = int(entry.get('id'))
 | 
				
			||||||
 | 
					            payload_by_id[iid] = int(entry.get('qty') or 1)
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    default_item_ids = set(Item.objects.filter(is_default_in_quotes=True).values_list('id', flat=True))
 | 
				
			||||||
 | 
					    if default_item_ids:
 | 
				
			||||||
 | 
					        for default_id in default_item_ids:
 | 
				
			||||||
 | 
					            if default_id not in payload_by_id:
 | 
				
			||||||
 | 
					                # مقدار پیش فرض را قرار بده
 | 
				
			||||||
 | 
					                default_qty = Item.objects.filter(id=default_id).values_list('default_quantity', flat=True).first() or 1
 | 
				
			||||||
 | 
					                payload_by_id[default_id] = int(default_qty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # بازسازی payload نهایی معتبر
 | 
				
			||||||
 | 
					    items_payload = [{'id': iid, 'qty': qty} for iid, qty in payload_by_id.items() if qty and qty > 0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not items_payload:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create or reuse quote
 | 
				
			||||||
 | 
					    quote, _ = Quote.objects.get_or_create(
 | 
				
			||||||
 | 
					        process_instance=instance,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            'name': f"پیشفاکتور {instance.code}",
 | 
				
			||||||
 | 
					            'customer': instance.representative or request.user,
 | 
				
			||||||
 | 
					            'valid_until': timezone.now().date(),
 | 
				
			||||||
 | 
					            'created_by': request.user,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Replace quote items with submitted ones
 | 
				
			||||||
 | 
					    quote.items.all().delete()
 | 
				
			||||||
 | 
					    for entry in items_payload:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            item_id = int(entry.get('id'))
 | 
				
			||||||
 | 
					            qty = int(entry.get('qty') or 1)
 | 
				
			||||||
 | 
					        except (TypeError, ValueError):
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        if qty <= 0:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        item = Item.objects.filter(id=item_id).first()
 | 
				
			||||||
 | 
					        if not item:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        QuoteItem.objects.create(
 | 
				
			||||||
 | 
					            quote=quote,
 | 
				
			||||||
 | 
					            item=item,
 | 
				
			||||||
 | 
					            quantity=qty,
 | 
				
			||||||
 | 
					            unit_price=item.unit_price,
 | 
				
			||||||
 | 
					            total_price=item.unit_price * qty,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    quote.calculate_totals()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # تکمیل مرحله
 | 
				
			||||||
 | 
					    step_instance, created = StepInstance.objects.get_or_create(
 | 
				
			||||||
 | 
					        process_instance=instance,
 | 
				
			||||||
 | 
					        step=step
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step_instance.status = 'completed'
 | 
				
			||||||
 | 
					    step_instance.completed_at = timezone.now()
 | 
				
			||||||
 | 
					    step_instance.save()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # انتقال به مرحله بعدی
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    redirect_url = None
 | 
				
			||||||
 | 
					    if next_step:
 | 
				
			||||||
 | 
					        instance.current_step = next_step
 | 
				
			||||||
 | 
					        instance.save()
 | 
				
			||||||
 | 
					        # هدایت مستقیم به مرحله پیشنمایش پیشفاکتور
 | 
				
			||||||
 | 
					        redirect_url = reverse('invoices:quote_preview_step', args=[instance.id, next_step.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'quote_id': quote.id, 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def quote_preview_step(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """مرحله صدور پیشفاکتور - نمایش و تایید فاکتور"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(
 | 
				
			||||||
 | 
					        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
				
			||||||
 | 
					        id=instance_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # بررسی دسترسی به مرحله
 | 
				
			||||||
 | 
					    if not instance.can_access_step(step):
 | 
				
			||||||
 | 
					        messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
 | 
				
			||||||
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # دریافت پیشفاکتور
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    step_instance = instance.step_instances.filter(step=step).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Navigation logic
 | 
				
			||||||
 | 
					    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render(request, 'invoices/quote_preview_step.html', {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'step': step,
 | 
				
			||||||
 | 
					        'step_instance': step_instance,
 | 
				
			||||||
 | 
					        'quote': quote,
 | 
				
			||||||
 | 
					        'previous_step': previous_step,
 | 
				
			||||||
 | 
					        'next_step': next_step,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def quote_print(request, instance_id):
 | 
				
			||||||
 | 
					    """صفحه پرینت پیشفاکتور"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render(request, 'invoices/quote_print.html', {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'quote': quote,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required  
 | 
				
			||||||
 | 
					def approve_quote(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """تایید پیشفاکتور و انتقال به مرحله بعدی"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # تایید پیشفاکتور
 | 
				
			||||||
 | 
					    quote.status = 'sent'
 | 
				
			||||||
 | 
					    quote.save()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # تکمیل مرحله
 | 
				
			||||||
 | 
					    step_instance, created = StepInstance.objects.get_or_create(
 | 
				
			||||||
 | 
					        process_instance=instance,
 | 
				
			||||||
 | 
					        step=step
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step_instance.status = 'completed'
 | 
				
			||||||
 | 
					    step_instance.completed_at = timezone.now()
 | 
				
			||||||
 | 
					    step_instance.save()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # انتقال به مرحله بعدی
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    redirect_url = None
 | 
				
			||||||
 | 
					    if next_step:
 | 
				
			||||||
 | 
					        instance.current_step = next_step
 | 
				
			||||||
 | 
					        instance.save()
 | 
				
			||||||
 | 
					        redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id])
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # در صورت نبود مرحله بعدی، بازگشت به لیست درخواستها
 | 
				
			||||||
 | 
					        redirect_url = reverse('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    messages.success(request, 'پیشفاکتور با موفقیت تایید شد.')
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'message': 'پیشفاکتور با موفقیت تایید شد.', 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """مرحله سوم: ثبت فیشهای واریزی پیشفاکتور"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(
 | 
				
			||||||
 | 
					        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
				
			||||||
 | 
					        id=instance_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # بررسی دسترسی
 | 
				
			||||||
 | 
					    if not instance.can_access_step(step):
 | 
				
			||||||
 | 
					        messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
 | 
				
			||||||
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    invoice = Invoice.objects.filter(quote=quote).first()
 | 
				
			||||||
 | 
					    payments = invoice.payments.select_related('created_by').filter(is_deleted=False).all() if invoice else []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    totals = {
 | 
				
			||||||
 | 
					        'final_amount': quote.final_amount,
 | 
				
			||||||
 | 
					        'paid_amount': quote.get_paid_amount(),
 | 
				
			||||||
 | 
					        'remaining_amount': quote.get_remaining_amount(),
 | 
				
			||||||
 | 
					        'is_fully_paid': quote.get_remaining_amount() <= 0,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    step_instance = instance.step_instances.filter(step=step).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return render(request, 'invoices/quote_payment_step.html', {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'step': step,
 | 
				
			||||||
 | 
					        'step_instance': step_instance,
 | 
				
			||||||
 | 
					        'quote': quote,
 | 
				
			||||||
 | 
					        'payments': payments,
 | 
				
			||||||
 | 
					        'totals': totals,
 | 
				
			||||||
 | 
					        'previous_step': previous_step,
 | 
				
			||||||
 | 
					        'next_step': next_step,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def add_quote_payment(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """افزودن فیش واریزی جدید برای پیشفاکتور"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    invoice, _ = Invoice.objects.get_or_create(
 | 
				
			||||||
 | 
					        process_instance=instance,
 | 
				
			||||||
 | 
					        quote=quote,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            'name': f"Invoice {quote.name}",
 | 
				
			||||||
 | 
					            'customer': quote.customer,
 | 
				
			||||||
 | 
					            'due_date': timezone.now().date(),
 | 
				
			||||||
 | 
					            'created_by': request.user,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        amount = (request.POST.get('amount') or '').strip()
 | 
				
			||||||
 | 
					        payment_date = (request.POST.get('payment_date') or '').strip()
 | 
				
			||||||
 | 
					        payment_method = (request.POST.get('payment_method') or '').strip()
 | 
				
			||||||
 | 
					        reference_number = (request.POST.get('reference_number') or '').strip()
 | 
				
			||||||
 | 
					        notes = (request.POST.get('notes') or '').strip()
 | 
				
			||||||
 | 
					        receipt_image = request.FILES.get('receipt_image')
 | 
				
			||||||
 | 
					        # Server-side validation for required fields
 | 
				
			||||||
 | 
					        if not amount:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'مبلغ را وارد کنید'})
 | 
				
			||||||
 | 
					        if not payment_date:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'تاریخ پرداخت را وارد کنید'})
 | 
				
			||||||
 | 
					        if not payment_method:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'روش پرداخت را انتخاب کنید'})
 | 
				
			||||||
 | 
					        if not reference_number:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'شماره مرجع را وارد کنید'})
 | 
				
			||||||
 | 
					        if not receipt_image:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'تصویر فیش را بارگذاری کنید'})
 | 
				
			||||||
 | 
					        # Normalize date to YYYY-MM-DD (accept YYYY/MM/DD from Persian datepicker)
 | 
				
			||||||
 | 
					        if '/' in payment_date:
 | 
				
			||||||
 | 
					            payment_date = payment_date.replace('/', '-')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Prevent overpayment
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            amount_dec = Decimal(amount)
 | 
				
			||||||
 | 
					        except InvalidOperation:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'مبلغ نامعتبر است'})
 | 
				
			||||||
 | 
					        remaining = quote.get_remaining_amount()
 | 
				
			||||||
 | 
					        if amount_dec > remaining:
 | 
				
			||||||
 | 
					            return JsonResponse({'success': False, 'message': 'مبلغ فیش بیشتر از مانده پیشفاکتور است'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Payment.objects.create(
 | 
				
			||||||
 | 
					            invoice=invoice,
 | 
				
			||||||
 | 
					            amount=amount_dec,
 | 
				
			||||||
 | 
					            payment_date=payment_date,
 | 
				
			||||||
 | 
					            payment_method=payment_method,
 | 
				
			||||||
 | 
					            reference_number=reference_number,
 | 
				
			||||||
 | 
					            receipt_image=receipt_image,
 | 
				
			||||||
 | 
					            notes=notes,
 | 
				
			||||||
 | 
					            created_by=request.user,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id)
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def update_quote_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    invoice = Invoice.objects.filter(quote=quote).first()
 | 
				
			||||||
 | 
					    if not invoice:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
 | 
				
			||||||
 | 
					    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        amount = request.POST.get('amount')
 | 
				
			||||||
 | 
					        payment_date = request.POST.get('payment_date') or payment.payment_date
 | 
				
			||||||
 | 
					        payment_method = request.POST.get('payment_method') or payment.payment_method
 | 
				
			||||||
 | 
					        reference_number = request.POST.get('reference_number') or ''
 | 
				
			||||||
 | 
					        notes = request.POST.get('notes') or ''
 | 
				
			||||||
 | 
					        receipt_image = request.FILES.get('receipt_image')
 | 
				
			||||||
 | 
					        if amount:
 | 
				
			||||||
 | 
					            payment.amount = amount
 | 
				
			||||||
 | 
					        payment.payment_date = payment_date
 | 
				
			||||||
 | 
					        payment.payment_method = payment_method
 | 
				
			||||||
 | 
					        payment.reference_number = reference_number
 | 
				
			||||||
 | 
					        payment.notes = notes
 | 
				
			||||||
 | 
					        # اگر نیاز به ذخیره عکس در Payment دارید، فیلد آن اضافه شده است
 | 
				
			||||||
 | 
					        if receipt_image:
 | 
				
			||||||
 | 
					            payment.receipt_image = receipt_image
 | 
				
			||||||
 | 
					        payment.save()
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					    invoice = Invoice.objects.filter(quote=quote).first()
 | 
				
			||||||
 | 
					    if not invoice:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
 | 
				
			||||||
 | 
					    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # soft delete using project's BaseModel delete override
 | 
				
			||||||
 | 
					        payment.delete()
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
 | 
				
			||||||
 | 
					    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def approve_payments(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """تایید مرحله پرداخت فیشها و انتقال به مرحله بعد"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    quote = get_object_or_404(Quote, process_instance=instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    is_fully_paid = quote.get_remaining_amount() <= 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # تکمیل مرحله
 | 
				
			||||||
 | 
					    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
				
			||||||
 | 
					    step_instance.status = 'completed'
 | 
				
			||||||
 | 
					    step_instance.completed_at = timezone.now()
 | 
				
			||||||
 | 
					    step_instance.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # حرکت به مرحله بعد
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    redirect_url = reverse('processes:request_list')
 | 
				
			||||||
 | 
					    if next_step:
 | 
				
			||||||
 | 
					        instance.current_step = next_step
 | 
				
			||||||
 | 
					        instance.save()
 | 
				
			||||||
 | 
					        redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    msg = 'پرداختها تایید شد'
 | 
				
			||||||
 | 
					    if is_fully_paid:
 | 
				
			||||||
 | 
					        msg += ' - مبلغ پیشفاکتور به طور کامل پرداخت شده است.'
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        msg += ' - توجه: مبلغ پیشفاکتور به طور کامل پرداخت نشده است.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return JsonResponse({'success': True, 'message': msg, 'redirect': redirect_url, 'is_fully_paid': is_fully_paid})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
					# Generated by Django 5.2.4 on 2025-08-14 09:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,13 +46,86 @@ class StepDependencyAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(ProcessInstance)
 | 
					@admin.register(ProcessInstance)
 | 
				
			||||||
class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
					class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
    list_display = ['name', 'process', 'requester', 'current_step', 'status', 'started_at', 'progress_display']
 | 
					    verbose_name = "درخواست"
 | 
				
			||||||
    list_filter = ['process', 'status', 'started_at']
 | 
					    verbose_name_plural = "درخواستها"
 | 
				
			||||||
    search_fields = ['name', 'process__name', 'requester__username', 'requester__first_name']
 | 
					    list_display = [
 | 
				
			||||||
    readonly_fields = ['deleted_at', 'started_at', 'completed_at']
 | 
					        'code',
 | 
				
			||||||
    ordering = ['-started_at']
 | 
					        'slug', 
 | 
				
			||||||
 | 
					        'well_display', 
 | 
				
			||||||
 | 
					        'representative', 
 | 
				
			||||||
 | 
					        'requester', 
 | 
				
			||||||
 | 
					        'process', 
 | 
				
			||||||
 | 
					        'status_display', 
 | 
				
			||||||
 | 
					        'priority_display', 
 | 
				
			||||||
 | 
					        'created', 
 | 
				
			||||||
 | 
					        'progress_display'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    list_filter = [
 | 
				
			||||||
 | 
					        'process', 
 | 
				
			||||||
 | 
					        'status', 
 | 
				
			||||||
 | 
					        'priority',
 | 
				
			||||||
 | 
					        'created',
 | 
				
			||||||
 | 
					        'well__representative'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    search_fields = [
 | 
				
			||||||
 | 
					        'code',
 | 
				
			||||||
 | 
					        'slug', 
 | 
				
			||||||
 | 
					        'process__name', 
 | 
				
			||||||
 | 
					        'requester__username', 
 | 
				
			||||||
 | 
					        'requester__first_name',
 | 
				
			||||||
 | 
					        'well__water_subscription_number',
 | 
				
			||||||
 | 
					        'representative__username'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    readonly_fields = [
 | 
				
			||||||
 | 
					        'deleted_at', 
 | 
				
			||||||
 | 
					        'created', 
 | 
				
			||||||
 | 
					        'updated',
 | 
				
			||||||
 | 
					        'completed_at'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    autocomplete_fields = [
 | 
				
			||||||
 | 
					        'well', 
 | 
				
			||||||
 | 
					        'representative', 
 | 
				
			||||||
 | 
					        'requester', 
 | 
				
			||||||
 | 
					        'process', 
 | 
				
			||||||
 | 
					        'current_step'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    ordering = ['-created']
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fieldsets = (
 | 
				
			||||||
 | 
					        ('اطلاعات اصلی', {
 | 
				
			||||||
 | 
					            'fields': ('code', 'slug', 'description', 'process')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات چاه', {
 | 
				
			||||||
 | 
					            'fields': ('well', 'representative')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات درخواست', {
 | 
				
			||||||
 | 
					            'fields': ('requester', 'priority')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('وضعیت و پیشرفت', {
 | 
				
			||||||
 | 
					            'fields': ('status', 'current_step')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('تاریخها', {
 | 
				
			||||||
 | 
					            'fields': ('created', 'updated', 'completed_at'),
 | 
				
			||||||
 | 
					            'classes': ('collapse',)
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def well_display(self, obj):
 | 
				
			||||||
 | 
					        if obj.well:
 | 
				
			||||||
 | 
					            return f"{obj.well.water_subscription_number}"
 | 
				
			||||||
 | 
					        return "-"
 | 
				
			||||||
 | 
					    well_display.short_description = "چاه"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def status_display(self, obj):
 | 
				
			||||||
 | 
					        return mark_safe(obj.get_status_display_with_color())
 | 
				
			||||||
 | 
					    status_display.short_description = "وضعیت"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def priority_display(self, obj):
 | 
				
			||||||
 | 
					        return mark_safe(obj.get_priority_display_with_color())
 | 
				
			||||||
 | 
					    priority_display.short_description = "اولویت"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def progress_display(self, obj):
 | 
					    def progress_display(self, obj):
 | 
				
			||||||
 | 
					        if obj.process:
 | 
				
			||||||
            total_steps = obj.process.steps.count()
 | 
					            total_steps = obj.process.steps.count()
 | 
				
			||||||
            completed_steps = obj.step_instances.filter(status='completed').count()
 | 
					            completed_steps = obj.step_instances.filter(status='completed').count()
 | 
				
			||||||
            percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
					            percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
				
			||||||
| 
						 | 
					@ -61,6 +134,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
                '<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
 | 
					                '<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
 | 
				
			||||||
                percentage_int, completed_steps, total_steps, percentage_int
 | 
					                percentage_int, completed_steps, total_steps, percentage_int
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        return "-"
 | 
				
			||||||
    progress_display.short_description = "پیشرفت"
 | 
					    progress_display.short_description = "پیشرفت"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(StepInstance)
 | 
					@admin.register(StepInstance)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,9 +4,16 @@ from .models import ProcessInstance, StepInstance
 | 
				
			||||||
class ProcessInstanceForm(forms.ModelForm):
 | 
					class ProcessInstanceForm(forms.ModelForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ProcessInstance
 | 
					        model = ProcessInstance
 | 
				
			||||||
        fields = ['name']
 | 
					        fields = ['description', 'process', 'well', 'representative', 'requester', 'priority', 'status', 'current_step']
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            'name': forms.TextInput(attrs={'class': 'form-control'})
 | 
					            'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
 | 
				
			||||||
 | 
					            'process': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'well': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'representative': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'requester': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'priority': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'status': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
 | 
					            'current_step': forms.Select(attrs={'class': 'form-control'}),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StepInstanceForm(forms.ModelForm):
 | 
					class StepInstanceForm(forms.ModelForm):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
					# Generated by Django 5.2.4 on 2025-08-14 09:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
import simple_history.models
 | 
					import simple_history.models
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ class Migration(migrations.Migration):
 | 
				
			||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('wells', '0001_initial'),
 | 
				
			||||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,18 +131,21 @@ class Migration(migrations.Migration):
 | 
				
			||||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
					                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
				
			||||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
					                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
				
			||||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
					                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
				
			||||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
					                ('code', models.CharField(help_text='کد ۵ رقمی یکتا برای هر درخواست', max_length=5, unique=True, verbose_name='کد درخواست')),
 | 
				
			||||||
 | 
					                ('description', models.TextField(blank=True, null=True, verbose_name='توضیحات درخواست')),
 | 
				
			||||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
					                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
				
			||||||
                ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
 | 
					                ('priority', models.CharField(choices=[('low', 'کم'), ('medium', 'متوسط'), ('high', 'زیاد'), ('urgent', 'فوری')], default='medium', max_length=20, verbose_name='اولویت')),
 | 
				
			||||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
					                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
				
			||||||
                ('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
 | 
					                ('process', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
 | 
				
			||||||
                ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
 | 
					                ('representative', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='representative_instances', to=settings.AUTH_USER_MODEL, verbose_name='نماینده چاه')),
 | 
				
			||||||
 | 
					                ('requester', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
 | 
				
			||||||
 | 
					                ('well', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='process_instances', to='wells.well', verbose_name='چاه')),
 | 
				
			||||||
                ('current_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='processes.processstep', verbose_name='مرحله فعلی')),
 | 
					                ('current_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='processes.processstep', verbose_name='مرحله فعلی')),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                'verbose_name': 'نمونه فرآیند',
 | 
					                'verbose_name': 'درخواست',
 | 
				
			||||||
                'verbose_name_plural': 'نمونه\u200cهای فرآیند',
 | 
					                'verbose_name_plural': 'درخواست\u200cها',
 | 
				
			||||||
                'ordering': ['-started_at'],
 | 
					                'ordering': ['-created'],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
| 
						 | 
					@ -169,37 +173,6 @@ class Migration(migrations.Migration):
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
					            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='HistoricalProcessInstance',
 | 
					 | 
				
			||||||
            fields=[
 | 
					 | 
				
			||||||
                ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
 | 
					 | 
				
			||||||
                ('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
 | 
					 | 
				
			||||||
                ('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
 | 
					 | 
				
			||||||
                ('is_active', models.BooleanField(default=True, verbose_name='فعال')),
 | 
					 | 
				
			||||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
					 | 
				
			||||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
					 | 
				
			||||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
					 | 
				
			||||||
                ('name', models.CharField(max_length=100, verbose_name='نام')),
 | 
					 | 
				
			||||||
                ('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
 | 
					 | 
				
			||||||
                ('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
 | 
					 | 
				
			||||||
                ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
 | 
					 | 
				
			||||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
					 | 
				
			||||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
					 | 
				
			||||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
					 | 
				
			||||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
					 | 
				
			||||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
					 | 
				
			||||||
                ('requester', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
 | 
					 | 
				
			||||||
                ('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
 | 
					 | 
				
			||||||
                ('current_step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله فعلی')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            options={
 | 
					 | 
				
			||||||
                'verbose_name': 'historical نمونه فرآیند',
 | 
					 | 
				
			||||||
                'verbose_name_plural': 'historical نمونه\u200cهای فرآیند',
 | 
					 | 
				
			||||||
                'ordering': ('-history_date', '-history_id'),
 | 
					 | 
				
			||||||
                'get_latest_by': ('history_date', 'history_id'),
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            bases=(simple_history.models.HistoricalChanges, models.Model),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name='StepInstance',
 | 
					            name='StepInstance',
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,11 @@
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from common.models import NameSlugModel
 | 
					from common.models import NameSlugModel, SluggedModel
 | 
				
			||||||
from simple_history.models import HistoricalRecords
 | 
					from simple_history.models import HistoricalRecords
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from _helpers.utils import generate_unique_slug
 | 
				
			||||||
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
User = get_user_model()
 | 
					User = get_user_model()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +24,7 @@ class Process(NameSlugModel):
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProcessStep(NameSlugModel):
 | 
					class ProcessStep(NameSlugModel):
 | 
				
			||||||
    """مدل مراحل فرآیند"""
 | 
					    """مدل مراحل فرآیند"""
 | 
				
			||||||
    process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
 | 
					    process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
 | 
				
			||||||
| 
						 | 
					@ -95,35 +99,169 @@ class StepDependency(models.Model):
 | 
				
			||||||
        if self.dependent_step.order <= self.dependency_step.order:
 | 
					        if self.dependent_step.order <= self.dependency_step.order:
 | 
				
			||||||
            raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
 | 
					            raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProcessInstance(NameSlugModel):
 | 
					
 | 
				
			||||||
 | 
					class ProcessInstance(SluggedModel):
 | 
				
			||||||
 | 
					    code = models.CharField(
 | 
				
			||||||
 | 
					        max_length=5,
 | 
				
			||||||
 | 
					        unique=True,
 | 
				
			||||||
 | 
					        verbose_name="کد درخواست",
 | 
				
			||||||
 | 
					        help_text="کد ۵ رقمی یکتا برای هر درخواست"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    """مدل نمونه فرآیند (برای هر درخواست)"""
 | 
					    """مدل نمونه فرآیند (برای هر درخواست)"""
 | 
				
			||||||
    process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='instances', verbose_name="فرآیند")
 | 
					
 | 
				
			||||||
    requester = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="درخواست کننده")
 | 
					    PRIORITY_CHOICES = [
 | 
				
			||||||
    current_step = models.ForeignKey('ProcessStep', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="مرحله فعلی")
 | 
					
 | 
				
			||||||
    status = models.CharField(
 | 
					        ('low', 'کم'),
 | 
				
			||||||
        max_length=20,
 | 
					        ('medium', 'متوسط'),
 | 
				
			||||||
        choices=[
 | 
					        ('high', 'زیاد'),
 | 
				
			||||||
 | 
					        ('urgent', 'فوری'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    STATUS_CHOICES = [
 | 
				
			||||||
        ('pending', 'در انتظار'),
 | 
					        ('pending', 'در انتظار'),
 | 
				
			||||||
        ('in_progress', 'در حال انجام'),
 | 
					        ('in_progress', 'در حال انجام'),
 | 
				
			||||||
        ('completed', 'تکمیل شده'),
 | 
					        ('completed', 'تکمیل شده'),
 | 
				
			||||||
        ('cancelled', 'لغو شده'),
 | 
					        ('cancelled', 'لغو شده'),
 | 
				
			||||||
        ('rejected', 'رد شده'),
 | 
					        ('rejected', 'رد شده'),
 | 
				
			||||||
        ],
 | 
					    ]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    description = models.TextField(
 | 
				
			||||||
 | 
					        verbose_name="توضیحات درخواست", 
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        null=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    process = models.ForeignKey(
 | 
				
			||||||
 | 
					        Process,
 | 
				
			||||||
 | 
					        on_delete=models.CASCADE,
 | 
				
			||||||
 | 
					        related_name='instances',
 | 
				
			||||||
 | 
					        verbose_name="فرآیند",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    well = models.ForeignKey(
 | 
				
			||||||
 | 
					        'wells.Well', 
 | 
				
			||||||
 | 
					        on_delete=models.CASCADE, 
 | 
				
			||||||
 | 
					        related_name='process_instances',
 | 
				
			||||||
 | 
					        verbose_name="چاه",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    representative = models.ForeignKey(
 | 
				
			||||||
 | 
					        User, 
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL, 
 | 
				
			||||||
 | 
					        related_name='representative_instances',
 | 
				
			||||||
 | 
					        verbose_name="نماینده چاه",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    requester = models.ForeignKey(
 | 
				
			||||||
 | 
					        User,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        verbose_name="درخواست کننده",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    current_step = models.ForeignKey(
 | 
				
			||||||
 | 
					        ProcessStep,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        verbose_name="مرحله فعلی",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    status = models.CharField(
 | 
				
			||||||
 | 
					        max_length=20,
 | 
				
			||||||
 | 
					        choices=STATUS_CHOICES,
 | 
				
			||||||
        default='pending',
 | 
					        default='pending',
 | 
				
			||||||
        verbose_name="وضعیت"
 | 
					        verbose_name="وضعیت"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
 | 
					    
 | 
				
			||||||
    completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
 | 
					    priority = models.CharField(
 | 
				
			||||||
    history = HistoricalRecords()
 | 
					        max_length=20,
 | 
				
			||||||
 | 
					        choices=PRIORITY_CHOICES,
 | 
				
			||||||
 | 
					        default='medium',
 | 
				
			||||||
 | 
					        verbose_name="اولویت"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    completed_at = models.DateTimeField(
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        verbose_name="تاریخ تکمیل"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = "نمونه فرآیند"
 | 
					        verbose_name = "درخواست"
 | 
				
			||||||
        verbose_name_plural = "نمونههای فرآیند"
 | 
					        verbose_name_plural = "درخواستها"
 | 
				
			||||||
        ordering = ['-started_at']
 | 
					        ordering = ['-created']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        if self.well:
 | 
				
			||||||
 | 
					            return f"{self.process.name} - {self.well.water_subscription_number}"
 | 
				
			||||||
        return f"{self.process.name} - {self.requester.get_full_name()}"
 | 
					        return f"{self.process.name} - {self.requester.get_full_name()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean(self):
 | 
				
			||||||
 | 
					        """اعتبارسنجی مدل"""
 | 
				
			||||||
 | 
					        if self.well and self.representative and self.well.representative != self.representative:
 | 
				
			||||||
 | 
					            raise ValidationError("نماینده درخواست باید همان نماینده ثبت شده در چاه باشد")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self.well and self.representative and self.requester == self.representative:
 | 
				
			||||||
 | 
					            raise ValidationError("درخواست کننده نمیتواند نماینده چاه باشد")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        # Generate unique 5-digit numeric code if missing
 | 
				
			||||||
 | 
					        if not self.code:
 | 
				
			||||||
 | 
					            # Try a few times to avoid rare collisions
 | 
				
			||||||
 | 
					            for _ in range(10):
 | 
				
			||||||
 | 
					                candidate = f"{random.randint(10000, 99999)}"
 | 
				
			||||||
 | 
					                if not ProcessInstance.objects.filter(code=candidate).exists():
 | 
				
			||||||
 | 
					                    self.code = candidate
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					            # As a fallback if collision persists (very unlikely)
 | 
				
			||||||
 | 
					            if not self.code:
 | 
				
			||||||
 | 
					                self.code = f"{random.randint(10000, 99999)}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not self.slug:
 | 
				
			||||||
 | 
					            slug_text = f"{self.process.name}-{self.well.water_subscription_number if self.well else 'unknown'}-{timezone.now().strftime('%Y%m%d')}"
 | 
				
			||||||
 | 
					            self.slug = generate_unique_slug(slug_text)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self.status == 'completed' and not self.completed_at:
 | 
				
			||||||
 | 
					            self.completed_at = timezone.now()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_status_display_with_color(self):
 | 
				
			||||||
 | 
					        """نمایش وضعیت با رنگ"""
 | 
				
			||||||
 | 
					        status_colors = {
 | 
				
			||||||
 | 
					            'pending': 'info',
 | 
				
			||||||
 | 
					            'in_progress': 'primary',
 | 
				
			||||||
 | 
					            'completed': 'success',
 | 
				
			||||||
 | 
					            'rejected': 'danger',
 | 
				
			||||||
 | 
					            'cancelled': 'warning',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        color = status_colors.get(self.status, 'secondary')
 | 
				
			||||||
 | 
					        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_priority_display_with_color(self):
 | 
				
			||||||
 | 
					        """نمایش اولویت با رنگ"""
 | 
				
			||||||
 | 
					        priority_colors = {
 | 
				
			||||||
 | 
					            'low': 'success',
 | 
				
			||||||
 | 
					            'medium': 'info',
 | 
				
			||||||
 | 
					            'high': 'warning',
 | 
				
			||||||
 | 
					            'urgent': 'danger',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        color = priority_colors.get(self.priority, 'secondary')
 | 
				
			||||||
 | 
					        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_priority_display())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def can_edit(self, user):
 | 
				
			||||||
 | 
					        """بررسی امکان ویرایش درخواست"""
 | 
				
			||||||
 | 
					        if self.status == 'pending' and self.requester == user:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self.representative == user and self.status in ['pending']:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_available_steps(self):
 | 
					    def get_available_steps(self):
 | 
				
			||||||
        """دریافت مراحل قابل دسترس"""
 | 
					        """دریافت مراحل قابل دسترس"""
 | 
				
			||||||
        available_steps = []
 | 
					        available_steps = []
 | 
				
			||||||
| 
						 | 
					@ -134,7 +272,6 @@ class ProcessInstance(NameSlugModel):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_access_step(self, step):
 | 
					    def can_access_step(self, step):
 | 
				
			||||||
        """بررسی امکان دسترسی به مرحله"""
 | 
					        """بررسی امکان دسترسی به مرحله"""
 | 
				
			||||||
        # بررسی وابستگیها
 | 
					 | 
				
			||||||
        dependencies = step.get_dependencies()
 | 
					        dependencies = step.get_dependencies()
 | 
				
			||||||
        for dependency_id in dependencies:
 | 
					        for dependency_id in dependencies:
 | 
				
			||||||
            step_instance = self.step_instances.filter(step_id=dependency_id).first()
 | 
					            step_instance = self.step_instances.filter(step_id=dependency_id).first()
 | 
				
			||||||
| 
						 | 
					@ -144,7 +281,6 @@ class ProcessInstance(NameSlugModel):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_edit_step(self, step):
 | 
					    def can_edit_step(self, step):
 | 
				
			||||||
        """بررسی امکان ویرایش مرحله"""
 | 
					        """بررسی امکان ویرایش مرحله"""
 | 
				
			||||||
        # اگر مرحله مسدود کننده باشد و مراحل بعدی تکمیل شده باشند
 | 
					 | 
				
			||||||
        if step.blocks_previous:
 | 
					        if step.blocks_previous:
 | 
				
			||||||
            later_steps = self.step_instances.filter(
 | 
					            later_steps = self.step_instances.filter(
 | 
				
			||||||
                step__order__gt=step.order,
 | 
					                step__order__gt=step.order,
 | 
				
			||||||
| 
						 | 
					@ -187,12 +323,10 @@ class StepInstance(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """ذخیره با اعتبارسنجی"""
 | 
					        """ذخیره با اعتبارسنجی"""
 | 
				
			||||||
        # بررسی وابستگیها
 | 
					 | 
				
			||||||
        if self.status == 'in_progress' or self.status == 'completed':
 | 
					        if self.status == 'in_progress' or self.status == 'completed':
 | 
				
			||||||
            if not self.process_instance.can_access_step(self.step):
 | 
					            if not self.process_instance.can_access_step(self.step):
 | 
				
			||||||
                raise ValidationError("مراحل وابسته تکمیل نشدهاند")
 | 
					                raise ValidationError("مراحل وابسته تکمیل نشدهاند")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # بررسی امکان ویرایش
 | 
					 | 
				
			||||||
        if self.status == 'completed' and not self.process_instance.can_edit_step(self.step):
 | 
					        if self.status == 'completed' and not self.process_instance.can_edit_step(self.step):
 | 
				
			||||||
            raise ValidationError("این مرحله قابل ویرایش نیست")
 | 
					            raise ValidationError("این مرحله قابل ویرایش نیست")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
| 
						 | 
					@ -252,7 +386,6 @@ class StepRejection(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """ذخیره با تغییر وضعیت مرحله"""
 | 
					        """ذخیره با تغییر وضعیت مرحله"""
 | 
				
			||||||
        # تغییر وضعیت مرحله به رد شده
 | 
					 | 
				
			||||||
        self.step_instance.status = 'rejected'
 | 
					        self.step_instance.status = 'rejected'
 | 
				
			||||||
        self.step_instance.save()
 | 
					        self.step_instance.save()
 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										54
									
								
								processes/templates/processes/includes/stepper_header.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								processes/templates/processes/includes/stepper_header.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="bs-stepper-header">
 | 
				
			||||||
 | 
					  {% for item in steps_context %}
 | 
				
			||||||
 | 
					    {% with step=item.step status=item.status can_access=item.can_access is_selected=item.is_selected is_todo=item.is_todo %}
 | 
				
			||||||
 | 
					    <div class="step 
 | 
				
			||||||
 | 
					      {% if not can_access %}disabled
 | 
				
			||||||
 | 
					      {% elif status == 'completed' %}completed
 | 
				
			||||||
 | 
					      {% elif is_todo %}active
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      {% if is_selected %} selected{% endif %}" 
 | 
				
			||||||
 | 
					      data-target="#step-{{ step.id }}">
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      {% if can_access %}
 | 
				
			||||||
 | 
					        <a href="{% url 'processes:step_detail' instance.id step.id %}" 
 | 
				
			||||||
 | 
					           class="step-trigger text-decoration-none p-0" 
 | 
				
			||||||
 | 
					           aria-selected="{% if is_selected %}true{% else %}false{% endif %}">
 | 
				
			||||||
 | 
					      {% else %}
 | 
				
			||||||
 | 
					        <span class="step-trigger">
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					        <span class="bs-stepper-circle">{{ forloop.counter }}</span>
 | 
				
			||||||
 | 
					        <span class="bs-stepper-label mt-1">
 | 
				
			||||||
 | 
					          <span class="bs-stepper-title">{{ step.name }}</span>
 | 
				
			||||||
 | 
					          <span class="bs-stepper-subtitle">{{ step.description|default:' ' }}</span>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					      {% if can_access %}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      {% else %}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    {% endwith %}
 | 
				
			||||||
 | 
					    {% if not forloop.last %}<div class="line"></div>{% endif %}
 | 
				
			||||||
 | 
					  {% endfor %}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.step.disabled .step-trigger { opacity: 0.5; cursor: not-allowed; }
 | 
				
			||||||
 | 
					.step.disabled .bs-stepper-circle { background-color: #6c757d; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* تکمیل شده */
 | 
				
			||||||
 | 
					.step.completed .bs-stepper-circle { background-color: #28a745 !important; color: #fff !important; border-color: #28a745 !important; }
 | 
				
			||||||
 | 
					.step.completed .bs-stepper-title { color: #28a745 !important; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* مرحلهای که باید انجام شود (فعلی سیستم) */
 | 
				
			||||||
 | 
					.step.active .bs-stepper-circle { background-color: #696cff; color: #fff; }
 | 
				
			||||||
 | 
					.step.active .bs-stepper-title { color: #696cff; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* مرحله انتخابشده (نمایش فعلی صفحه) */
 | 
				
			||||||
 | 
					.step.selected { outline: 1px solid #696cff; border-radius: 8px; }
 | 
				
			||||||
 | 
					.step.selected .bs-stepper-circle { box-shadow: 0 0 0 3px rgba(13,202,240,.25); }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										774
									
								
								processes/templates/processes/request_list.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										774
									
								
								processes/templates/processes/request_list.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,774 @@
 | 
				
			||||||
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block sidebar %}
 | 
				
			||||||
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock sidebar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block navbar %}
 | 
				
			||||||
 | 
					    {% include 'navbars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock navbar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}درخواستها{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block style %}
 | 
				
			||||||
 | 
					<!-- DataTables CSS -->
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-bs5/datatables.bootstrap5.css' %}">
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.css' %}">
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.css' %}">
 | 
				
			||||||
 | 
					<!-- Persian Date Picker CSS -->
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="https://unpkg.com/persian-datepicker@latest/dist/css/persian-datepicker.min.css">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock style %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% include '_toasts.html' %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="container-xxl flex-grow-1 container-p-y">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="d-flex align-items-center justify-content-between mb-3">
 | 
				
			||||||
 | 
					    <h4 class="mb-0">درخواستها</h4>
 | 
				
			||||||
 | 
					    <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
 | 
				
			||||||
 | 
					      <i class="bx bx-plus"></i>
 | 
				
			||||||
 | 
					      درخواست جدید
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="card">
 | 
				
			||||||
 | 
					    <div class="table-responsive">
 | 
				
			||||||
 | 
					      <table id="requestsTable" class="table table-striped">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>شناسه</th>
 | 
				
			||||||
 | 
					            <th>فرآیند</th>
 | 
				
			||||||
 | 
					            <th>شماره اشتراک آب</th>
 | 
				
			||||||
 | 
					            <th>نماینده</th>
 | 
				
			||||||
 | 
					            <th>درخواستکننده</th>
 | 
				
			||||||
 | 
					            <th>اولویت</th>
 | 
				
			||||||
 | 
					            <th>وضعیت</th>
 | 
				
			||||||
 | 
					            <th>تاریخ ایجاد</th>
 | 
				
			||||||
 | 
					            <th>عملیات</th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					          {% for inst in instances %}
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td>{{ inst.code }}</td>
 | 
				
			||||||
 | 
					            <td>{{ inst.process.name }}</td>
 | 
				
			||||||
 | 
					            <td>{{ inst.well.water_subscription_number }}</td>
 | 
				
			||||||
 | 
					            <td>{% if inst.representative %}{{ inst.representative.get_full_name }}{% else %}-{% endif %}</td>
 | 
				
			||||||
 | 
					            <td>{% if inst.requester %}{{ inst.requester.get_full_name }}{% else %}-{% endif %}</td>
 | 
				
			||||||
 | 
					            <td>{{ inst.get_priority_display }}</td>
 | 
				
			||||||
 | 
					            <td>{{ inst.get_status_display }}</td>
 | 
				
			||||||
 | 
					            <td>{{ inst.jcreated }}</td>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					              <div class="d-inline-block">
 | 
				
			||||||
 | 
					                <a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
 | 
				
			||||||
 | 
					                  <i class="icon-base bx bx-dots-vertical-rounded"></i>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                <ul class="dropdown-menu dropdown-menu-end m-0">
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    <a href="{% url 'processes:instance_steps' inst.id %}" class="dropdown-item">
 | 
				
			||||||
 | 
					                      <i class="bx bx-show me-1"></i>مشاهده جزئیات
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                  <div class="dropdown-divider"></div>
 | 
				
			||||||
 | 
					                  <li>
 | 
				
			||||||
 | 
					                    <a href="#" class="dropdown-item text-danger" data-instance-id="{{ inst.id }}" data-instance-code="{{ inst.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
 | 
				
			||||||
 | 
					                      <i class="bx bx-trash me-1"></i>حذف
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                  </li>
 | 
				
			||||||
 | 
					                </ul>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          {% empty %}
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td colspan="9" class="text-center text-muted">موردی ثبت نشده است</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          {% endfor %}
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Modal -->
 | 
				
			||||||
 | 
					  <div class="modal fade" id="requestModal" tabindex="-1" aria-hidden="true">
 | 
				
			||||||
 | 
					    <div class="modal-dialog modal-lg modal-dialog-centered">
 | 
				
			||||||
 | 
					      <div class="modal-content">
 | 
				
			||||||
 | 
					        <div class="modal-header">
 | 
				
			||||||
 | 
					          <h5 class="modal-title">درخواست جدید</h5>
 | 
				
			||||||
 | 
					          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="modal-body">
 | 
				
			||||||
 | 
					          <form id="requestForm">
 | 
				
			||||||
 | 
					            {% csrf_token %}
 | 
				
			||||||
 | 
					            <div class="row g-3">
 | 
				
			||||||
 | 
					              <div class="col-sm-12">
 | 
				
			||||||
 | 
					                <label class="form-label">فرآیند</label>
 | 
				
			||||||
 | 
					                <select class="form-select" name="process" id="req_process" required>
 | 
				
			||||||
 | 
					                  {% for process in processes %}
 | 
				
			||||||
 | 
					                  <option value="{{ process.id }}">{{ process.name }}</option>
 | 
				
			||||||
 | 
					                  {% endfor %}
 | 
				
			||||||
 | 
					                </select>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <hr class="mt-3 border border-dashed">
 | 
				
			||||||
 | 
					              <div class="col-sm-12">
 | 
				
			||||||
 | 
					                <label class="form-label">شماره اشتراک آب</label>
 | 
				
			||||||
 | 
					                <div class="input-group">
 | 
				
			||||||
 | 
					                  <input type="text" class="form-control" id="req_water_sub" placeholder="مثال: 12345" required>
 | 
				
			||||||
 | 
					                  <button class="btn btn-outline-secondary" type="button" id="btnLookupWell">
 | 
				
			||||||
 | 
					                    بررسی/افزودن چاه
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="form-text" id="wellStatus"></div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <!-- Well form fields (from WellForm) -->
 | 
				
			||||||
 | 
					              <div id="wellFormBlock" class="col-sm-12" style="display:none;">
 | 
				
			||||||
 | 
					                <div class="row g-3">
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_electricity_subscription_number">{{ well_form.electricity_subscription_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.electricity_subscription_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_water_meter_manufacturer">{{ well_form.water_meter_manufacturer.label }}</label>
 | 
				
			||||||
 | 
					                    <div class="input-group">
 | 
				
			||||||
 | 
					                      <select name="water_meter_manufacturer" class="form-select" id="id_water_meter_manufacturer">
 | 
				
			||||||
 | 
					                        <option value="" selected="">انتخاب شرکت سازنده</option>
 | 
				
			||||||
 | 
					                        {% for manufacturer in manufacturers %}
 | 
				
			||||||
 | 
					                        <option value="{{ manufacturer.id }}">{{ manufacturer.name }}</option>
 | 
				
			||||||
 | 
					                        {% endfor %}
 | 
				
			||||||
 | 
					                      </select>
 | 
				
			||||||
 | 
					                      <input type="text" class="form-control" id="id_new_manufacturer" name="new_manufacturer" placeholder="شرکت سازنده جدید" style="display:none;">
 | 
				
			||||||
 | 
					                      <button class="btn btn-outline-primary" type="button" id="btnToggleManufacturer">
 | 
				
			||||||
 | 
					                        <i class="bx bx-plus"></i>
 | 
				
			||||||
 | 
					                      </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_water_meter_serial_number">{{ well_form.water_meter_serial_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.water_meter_serial_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_water_meter_old_serial_number">{{ well_form.water_meter_old_serial_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.water_meter_old_serial_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_utm_x">{{ well_form.utm_x.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.utm_x }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_utm_y">{{ well_form.utm_y.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.utm_y }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_utm_zone">{{ well_form.utm_zone.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.utm_zone }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_utm_hemisphere">{{ well_form.utm_hemisphere.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.utm_hemisphere }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_well_power">{{ well_form.well_power.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.well_power }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6"></div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_reference_letter_number">{{ well_form.reference_letter_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.reference_letter_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_reference_letter_date">{{ well_form.reference_letter_date.label }}</label>
 | 
				
			||||||
 | 
					                    <input type="text" class="form-control" id="id_reference_letter_date" name="reference_letter_date" placeholder="انتخاب تاریخ" readonly>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-12">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_representative_letter_file">{{ well_form.representative_letter_file.label }}</label>
 | 
				
			||||||
 | 
					                    {{ well_form.representative_letter_file }}
 | 
				
			||||||
 | 
					                    <!-- نمایش فایل موجود -->
 | 
				
			||||||
 | 
					                    <div id="current-file-display" style="display: none; margin-top: 10px;">
 | 
				
			||||||
 | 
					                      <div class="alert alert-info d-flex align-items-center justify-content-between">
 | 
				
			||||||
 | 
					                        <div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                          <i class="bx bx-file me-2"></i>
 | 
				
			||||||
 | 
					                          <span id="current-file-name" class="text-truncate" style="max-width: 200px;" title=""></span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <button type="button" class="btn btn-sm btn-outline-danger" onclick="removeCurrentFile()">
 | 
				
			||||||
 | 
					                          <i class="bx bx-trash me-1"></i>حذف
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <input type="hidden" id="remove-file" name="remove_file" value="false">
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <hr class="mt-3 border border-dashed">
 | 
				
			||||||
 | 
					              <div class="col-sm-12">
 | 
				
			||||||
 | 
					                <label class="form-label">کد ملی نماینده</label>
 | 
				
			||||||
 | 
					                <div class="input-group">
 | 
				
			||||||
 | 
					                  <input type="text" class="form-control" id="rep_national_code" placeholder="مثال: 0012345678">
 | 
				
			||||||
 | 
					                  <button class="btn btn-outline-secondary" type="button" id="btnLookupRep">
 | 
				
			||||||
 | 
					                    بررسی/افزودن نماینده
 | 
				
			||||||
 | 
					                  </button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="form-text" id="repStatus"></div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div id="repNewFields" class="col-sm-12" style="display:none;">
 | 
				
			||||||
 | 
					                <div class="row g-3">
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_first_name">{{ customer_form.first_name.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.first_name }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_last_name">{{ customer_form.last_name.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.last_name }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_phone_number_1">{{ customer_form.phone_number_1.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.phone_number_1 }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_phone_number_2">{{ customer_form.phone_number_2.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.phone_number_2 }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_national_code">{{ customer_form.national_code.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.national_code }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_card_number">{{ customer_form.card_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.card_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_account_number">{{ customer_form.account_number.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.account_number }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-12">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_address">{{ customer_form.address.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.address }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <hr class="mt-3 border border-dashed">
 | 
				
			||||||
 | 
					              <div class="col-sm-12">
 | 
				
			||||||
 | 
					                <label class="form-label">توضیحات</label>
 | 
				
			||||||
 | 
					                <textarea class="form-control" rows="3" id="req_description"></textarea>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="modal-footer">
 | 
				
			||||||
 | 
					          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">بستن</button>
 | 
				
			||||||
 | 
					          <button type="button" class="btn btn-primary" id="btnSaveRequest" disabled>ذخیره</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- Delete Confirmation Modal -->
 | 
				
			||||||
 | 
					<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
 | 
				
			||||||
 | 
					  <div class="modal-dialog">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <div class="modal-header">
 | 
				
			||||||
 | 
					        <h5 class="modal-title" id="deleteConfirmModalLabel">تایید حذف</h5>
 | 
				
			||||||
 | 
					        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-body">
 | 
				
			||||||
 | 
					        <p id="deleteConfirmText">آیا از حذف این درخواست اطمینان دارید؟</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="modal-footer">
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-danger" id="confirmDeleteBtn">حذف</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block script %}
 | 
				
			||||||
 | 
					<!-- DataTables JS -->
 | 
				
			||||||
 | 
					<script src="{% static 'assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js' %}"></script>
 | 
				
			||||||
 | 
					<!-- Persian DataTable defaults -->
 | 
				
			||||||
 | 
					<script src="{% static 'assets/js/persian-datatable.js' %}"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- Persian Date Picker JS -->
 | 
				
			||||||
 | 
					<script src="https://unpkg.com/persian-date@latest/dist/persian-date.min.js"></script>
 | 
				
			||||||
 | 
					<script src="https://unpkg.com/persian-datepicker@latest/dist/js/persian-datepicker.min.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Function to initialize Persian Date Picker
 | 
				
			||||||
 | 
					  function initPersianDatePicker() {
 | 
				
			||||||
 | 
					    if ($.fn.persianDatepicker && $('#id_reference_letter_date').length) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        $('#id_reference_letter_date').persianDatepicker({
 | 
				
			||||||
 | 
					          format: 'YYYY/MM/DD',
 | 
				
			||||||
 | 
					          initialValue: false,
 | 
				
			||||||
 | 
					          autoClose: true,
 | 
				
			||||||
 | 
					          persianDigit: false,
 | 
				
			||||||
 | 
					          observer: true,
 | 
				
			||||||
 | 
					          calendar: {
 | 
				
			||||||
 | 
					            persian: {
 | 
				
			||||||
 | 
					              locale: 'fa',
 | 
				
			||||||
 | 
					              leapYearMode: 'astronomical'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onSelect: function(unix) {
 | 
				
			||||||
 | 
					            // تبدیل تاریخ شمسی به میلادی برای ارسال به سرور
 | 
				
			||||||
 | 
					            const gregorianDate = new Date(unix);
 | 
				
			||||||
 | 
					            const year = gregorianDate.getFullYear();
 | 
				
			||||||
 | 
					            const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
 | 
				
			||||||
 | 
					            const day = String(gregorianDate.getDate()).padStart(2, '0');
 | 
				
			||||||
 | 
					            const gregorianDateString = `${year}-${month}-${day}`;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // نمایش تاریخ شمسی در فیلد
 | 
				
			||||||
 | 
					            if (window.persianDate) {
 | 
				
			||||||
 | 
					              const persianDate = new window.persianDate(unix);
 | 
				
			||||||
 | 
					              const persianDateString = persianDate.format('YYYY/MM/DD');
 | 
				
			||||||
 | 
					              $('#id_reference_letter_date').val(persianDateString);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
 | 
				
			||||||
 | 
					              $('#id_reference_letter_date').val(gregorianDateString);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
 | 
				
			||||||
 | 
					            $('#id_reference_letter_date').attr('data-gregorian', gregorianDateString);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error('Error initializing Persian Date Picker:', e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $(function() {
 | 
				
			||||||
 | 
					    // if ($.fn.DataTable) {
 | 
				
			||||||
 | 
					    //   try {
 | 
				
			||||||
 | 
					    //     $('#requestsTable').DataTable({
 | 
				
			||||||
 | 
					    //       pageLength: 10,
 | 
				
			||||||
 | 
					    //       order: [[0, 'desc']]
 | 
				
			||||||
 | 
					    //     });
 | 
				
			||||||
 | 
					    //   } catch (e) {
 | 
				
			||||||
 | 
					    //     console.error('DataTable init failed', e);
 | 
				
			||||||
 | 
					    //   }
 | 
				
			||||||
 | 
					    // } else {
 | 
				
			||||||
 | 
					    //   console.warn('DataTables library not loaded');
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let currentWellId = null;
 | 
				
			||||||
 | 
					    let currentRepId = null;
 | 
				
			||||||
 | 
					    let wellChecked = false;
 | 
				
			||||||
 | 
					    let repChecked = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function setStatus(el, text, type) {
 | 
				
			||||||
 | 
					      $(el).text(text).removeClass('text-danger text-success text-muted').addClass(type ? 'text-' + type : 'text-muted');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function checkSaveButton() {
 | 
				
			||||||
 | 
					      const canSave = wellChecked && repChecked;
 | 
				
			||||||
 | 
					      $('#btnSaveRequest').prop('disabled', !canSave);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Inline error helpers
 | 
				
			||||||
 | 
					    function clearInlineErrors() {
 | 
				
			||||||
 | 
					      $('#requestModal .is-invalid').removeClass('is-invalid');
 | 
				
			||||||
 | 
					      $('#requestModal .invalid-feedback.inline-error').remove();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function applyErrorTo(selector, message) {
 | 
				
			||||||
 | 
					      const $el = $(selector);
 | 
				
			||||||
 | 
					      if (!$el.length) return false;
 | 
				
			||||||
 | 
					      $el.addClass('is-invalid');
 | 
				
			||||||
 | 
					      const $feedback = $('<div class="invalid-feedback inline-error"></div>').text(message);
 | 
				
			||||||
 | 
					      const $grp = $el.closest('.input-group');
 | 
				
			||||||
 | 
					      if ($grp.length) {
 | 
				
			||||||
 | 
					        $feedback.insertAfter($grp);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        $feedback.insertAfter($el);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function mapWellFieldToSelector(field) {
 | 
				
			||||||
 | 
					      switch (field) {
 | 
				
			||||||
 | 
					        case 'water_subscription_number': return '#req_water_sub';
 | 
				
			||||||
 | 
					        case 'electricity_subscription_number': return '#id_electricity_subscription_number';
 | 
				
			||||||
 | 
					        case 'water_meter_serial_number': return '#id_water_meter_serial_number';
 | 
				
			||||||
 | 
					        case 'water_meter_old_serial_number': return '#id_water_meter_old_serial_number';
 | 
				
			||||||
 | 
					        case 'water_meter_manufacturer': return '#id_water_meter_manufacturer';
 | 
				
			||||||
 | 
					        case 'new_manufacturer': return '#id_new_manufacturer';
 | 
				
			||||||
 | 
					        case 'utm_x': return '#id_utm_x';
 | 
				
			||||||
 | 
					        case 'utm_y': return '#id_utm_y';
 | 
				
			||||||
 | 
					        case 'utm_zone': return '#id_utm_zone';
 | 
				
			||||||
 | 
					        case 'utm_hemisphere': return '#id_utm_hemisphere';
 | 
				
			||||||
 | 
					        case 'well_power': return '#id_well_power';
 | 
				
			||||||
 | 
					        case 'reference_letter_number': return '#id_reference_letter_number';
 | 
				
			||||||
 | 
					        case 'reference_letter_date': return '#id_reference_letter_date';
 | 
				
			||||||
 | 
					        case 'representative_letter_file': return '#id_representative_letter_file';
 | 
				
			||||||
 | 
					        case 'representative': return '#rep_national_code';
 | 
				
			||||||
 | 
					        default: return '#id_' + field;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function mapCustomerFieldToSelector(field) {
 | 
				
			||||||
 | 
					      switch (field) {
 | 
				
			||||||
 | 
					        case 'national_code': return $('#id_national_code').length ? '#id_national_code' : '#rep_national_code';
 | 
				
			||||||
 | 
					        case 'first_name': return '#id_first_name';
 | 
				
			||||||
 | 
					        case 'last_name': return '#id_last_name';
 | 
				
			||||||
 | 
					        case 'phone_number_1': return '#id_phone_number_1';
 | 
				
			||||||
 | 
					        case 'phone_number_2': return '#id_phone_number_2';
 | 
				
			||||||
 | 
					        case 'card_number': return '#id_card_number';
 | 
				
			||||||
 | 
					        case 'account_number': return '#id_account_number';
 | 
				
			||||||
 | 
					        case 'address': return '#id_address';
 | 
				
			||||||
 | 
					        default: return '#id_' + field;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function showInlineErrors(errors) {
 | 
				
			||||||
 | 
					      if (!errors) return;
 | 
				
			||||||
 | 
					      let nonFieldWell = '';
 | 
				
			||||||
 | 
					      let nonFieldCustomer = '';
 | 
				
			||||||
 | 
					      if (errors.well) {
 | 
				
			||||||
 | 
					        for (const key in errors.well) {
 | 
				
			||||||
 | 
					          const msgs = Array.isArray(errors.well[key]) ? errors.well[key] : [errors.well[key]];
 | 
				
			||||||
 | 
					          if (key === '__all__' || key === 'non_field_errors') { nonFieldWell = msgs.join('، '); continue; }
 | 
				
			||||||
 | 
					          const sel = mapWellFieldToSelector(key);
 | 
				
			||||||
 | 
					          applyErrorTo(sel, msgs[0]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (errors.customer) {
 | 
				
			||||||
 | 
					        for (const key in errors.customer) {
 | 
				
			||||||
 | 
					          const msgs = Array.isArray(errors.customer[key]) ? errors.customer[key] : [errors.customer[key]];
 | 
				
			||||||
 | 
					          if (key === '__all__' || key === 'non_field_errors') { nonFieldCustomer = msgs.join('، '); continue; }
 | 
				
			||||||
 | 
					          const sel = mapCustomerFieldToSelector(key);
 | 
				
			||||||
 | 
					          applyErrorTo(sel, msgs[0]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (nonFieldWell) setStatus('#wellStatus', nonFieldWell, 'danger');
 | 
				
			||||||
 | 
					      if (nonFieldCustomer) setStatus('#repStatus', nonFieldCustomer, 'danger');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#btnLookupWell').on('click', function() {
 | 
				
			||||||
 | 
					      const sub = $('#req_water_sub').val().trim();
 | 
				
			||||||
 | 
					      if (!sub) { setStatus('#wellStatus', 'لطفا شماره اشتراک آب را وارد کنید', 'danger'); return; }
 | 
				
			||||||
 | 
					      setStatus('#wellStatus', 'در حال بررسی...', 'muted');
 | 
				
			||||||
 | 
					      wellChecked = true;
 | 
				
			||||||
 | 
					      checkSaveButton();
 | 
				
			||||||
 | 
					      $.get('{% url "processes:lookup_well_by_subscription" %}', { water_subscription_number: sub })
 | 
				
			||||||
 | 
					        .done(function(resp){
 | 
				
			||||||
 | 
					            if (resp.exists) {
 | 
				
			||||||
 | 
					            currentWellId = resp.well.id;
 | 
				
			||||||
 | 
					            $('#wellFormBlock').show();
 | 
				
			||||||
 | 
					            // Initialize Persian Date Picker after well form is shown
 | 
				
			||||||
 | 
					            setTimeout(initPersianDatePicker, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Prefill well form
 | 
				
			||||||
 | 
					            $('#id_electricity_subscription_number').val(resp.well.electricity_subscription_number || '');
 | 
				
			||||||
 | 
					            $('#id_water_meter_serial_number').val(resp.well.water_meter_serial_number || '');
 | 
				
			||||||
 | 
					            $('#id_water_meter_old_serial_number').val(resp.well.water_meter_old_serial_number || '');
 | 
				
			||||||
 | 
					            $('#id_water_meter_manufacturer').val(resp.well.water_meter_manufacturer || '');
 | 
				
			||||||
 | 
					            $('#id_utm_x').val(resp.well.utm_x || '');
 | 
				
			||||||
 | 
					            $('#id_utm_y').val(resp.well.utm_y || '');
 | 
				
			||||||
 | 
					            $('#id_utm_zone').val(resp.well.utm_zone || '');
 | 
				
			||||||
 | 
					            $('#id_utm_hemisphere').val(resp.well.utm_hemisphere || '');
 | 
				
			||||||
 | 
					            $('#id_well_power').val(resp.well.well_power || '');
 | 
				
			||||||
 | 
					            $('#id_reference_letter_number').val(resp.well.reference_letter_number || '');
 | 
				
			||||||
 | 
					            // Prefill date: show Persian in input, keep Gregorian in data attribute
 | 
				
			||||||
 | 
					            if (resp.well.reference_letter_date) {
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                if (window.persianDate) {
 | 
				
			||||||
 | 
					                  const gregorianDate = new Date(resp.well.reference_letter_date);
 | 
				
			||||||
 | 
					                  const persianDateObj = new window.persianDate(gregorianDate);
 | 
				
			||||||
 | 
					                  const persianDateString = persianDateObj.format('YYYY/MM/DD');
 | 
				
			||||||
 | 
					                  $('#id_reference_letter_date').val(persianDateString);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  $('#id_reference_letter_date').val(resp.well.reference_letter_date);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $('#id_reference_letter_date').attr('data-gregorian', resp.well.reference_letter_date);
 | 
				
			||||||
 | 
					              } catch (e) {
 | 
				
			||||||
 | 
					                $('#id_reference_letter_date').val(resp.well.reference_letter_date);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              $('#id_reference_letter_date').val('');
 | 
				
			||||||
 | 
					              $('#id_reference_letter_date').removeAttr('data-gregorian');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					              // Existing representative letter file display
 | 
				
			||||||
 | 
					              if (resp.well.representative_letter_file_url) {
 | 
				
			||||||
 | 
					                $('#current-file-display').show();
 | 
				
			||||||
 | 
					                const fileName = resp.well.representative_letter_file_name || 'فایل موجود';
 | 
				
			||||||
 | 
					                $('#current-file-name').text(fileName).attr('title', fileName);
 | 
				
			||||||
 | 
					                $('#id_representative_letter_file').hide();
 | 
				
			||||||
 | 
					                $('#remove-file').val('false');
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                $('#current-file-display').hide();
 | 
				
			||||||
 | 
					                $('#id_representative_letter_file').show();
 | 
				
			||||||
 | 
					                $('#remove-file').val('false');
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            setStatus('#wellStatus', 'چاه یافت شد', 'success');
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            currentWellId = null;
 | 
				
			||||||
 | 
					            $('#wellFormBlock').show();
 | 
				
			||||||
 | 
					            $('#wellFormBlock').find('input, select').val('');
 | 
				
			||||||
 | 
					            $('#id_reference_letter_date').removeAttr('data-gregorian');
 | 
				
			||||||
 | 
					              // Reset file UI for new well
 | 
				
			||||||
 | 
					              $('#current-file-display').hide();
 | 
				
			||||||
 | 
					              $('#id_representative_letter_file').show().val('');
 | 
				
			||||||
 | 
					              $('#remove-file').val('false');
 | 
				
			||||||
 | 
					            // Initialize Persian Date Picker after well form is shown
 | 
				
			||||||
 | 
					            setTimeout(initPersianDatePicker, 100);
 | 
				
			||||||
 | 
					            setStatus('#wellStatus', 'چاه یافت نشد. با ذخیره، ایجاد خواهد شد.', 'danger');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#btnLookupRep').on('click', function() {
 | 
				
			||||||
 | 
					      const nc = $('#rep_national_code').val().trim();
 | 
				
			||||||
 | 
					      if (!nc) { setStatus('#repStatus', 'لطفا کد ملی نماینده را وارد کنید', 'danger'); return; }
 | 
				
			||||||
 | 
					      setStatus('#repStatus', 'در حال بررسی...', 'muted');
 | 
				
			||||||
 | 
					      repChecked = true;
 | 
				
			||||||
 | 
					      checkSaveButton();
 | 
				
			||||||
 | 
					      $.get('{% url "processes:lookup_representative_by_national_code" %}', { national_code: nc })
 | 
				
			||||||
 | 
					        .done(function(resp){
 | 
				
			||||||
 | 
					          if (resp.exists) {
 | 
				
			||||||
 | 
					            currentRepId = resp.user.id;
 | 
				
			||||||
 | 
					            $('#repNewFields').show();
 | 
				
			||||||
 | 
					            // Prefill customer form fields for editing
 | 
				
			||||||
 | 
					            $('#id_first_name').val(resp.user.first_name || '');
 | 
				
			||||||
 | 
					            $('#id_last_name').val(resp.user.last_name || '');
 | 
				
			||||||
 | 
					            if (resp.user.profile) {
 | 
				
			||||||
 | 
					              $('#id_national_code').val(resp.user.profile.national_code || nc);
 | 
				
			||||||
 | 
					              $('#id_phone_number_1').val(resp.user.profile.phone_number_1 || '');
 | 
				
			||||||
 | 
					              $('#id_phone_number_2').val(resp.user.profile.phone_number_2 || '');
 | 
				
			||||||
 | 
					              $('#id_card_number').val(resp.user.profile.card_number || '');
 | 
				
			||||||
 | 
					              $('#id_account_number').val(resp.user.profile.account_number || '');
 | 
				
			||||||
 | 
					              $('#id_address').val(resp.user.profile.address || '');
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              $('#id_national_code').val(nc);
 | 
				
			||||||
 | 
					              $('#id_phone_number_1').val('');
 | 
				
			||||||
 | 
					              $('#id_phone_number_2').val('');
 | 
				
			||||||
 | 
					              $('#id_card_number').val('');
 | 
				
			||||||
 | 
					              $('#id_account_number').val('');
 | 
				
			||||||
 | 
					              $('#id_address').val('');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            setStatus('#repStatus', 'نماینده یافت شد.', 'success');
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            currentRepId = null;
 | 
				
			||||||
 | 
					            $('#repNewFields').show();
 | 
				
			||||||
 | 
					            // Clear form and prefill national code
 | 
				
			||||||
 | 
					            $('#id_first_name').val('');
 | 
				
			||||||
 | 
					            $('#id_last_name').val('');
 | 
				
			||||||
 | 
					            $('#id_national_code').val(nc);
 | 
				
			||||||
 | 
					            $('#id_phone_number_1').val('');
 | 
				
			||||||
 | 
					            $('#id_phone_number_2').val('');
 | 
				
			||||||
 | 
					            $('#id_card_number').val('');
 | 
				
			||||||
 | 
					            $('#id_account_number').val('');
 | 
				
			||||||
 | 
					            $('#id_address').val('');
 | 
				
			||||||
 | 
					            setStatus('#repStatus', 'نماینده یافت نشد. لطفا اطلاعات را تکمیل کنید.', 'danger');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .fail(function(){ setStatus('#repStatus', 'خطا در بررسی نماینده', 'danger'); });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#btnSaveRequest').on('click', function(){
 | 
				
			||||||
 | 
					      const formData = new FormData();
 | 
				
			||||||
 | 
					      formData.append('csrfmiddlewaretoken', $('input[name=csrfmiddlewaretoken]').val());
 | 
				
			||||||
 | 
					      formData.append('process', $('#req_process').val());
 | 
				
			||||||
 | 
					      formData.append('description', $('#req_description').val());
 | 
				
			||||||
 | 
					      formData.append('water_subscription_number', $('#req_water_sub').val().trim());
 | 
				
			||||||
 | 
					      if (currentWellId) formData.append('well_id', currentWellId);
 | 
				
			||||||
 | 
					      if (currentRepId) formData.append('representative_id', currentRepId);
 | 
				
			||||||
 | 
					      // Send fields using CustomerForm names if visible
 | 
				
			||||||
 | 
					      const ncField = $('#id_national_code').length ? $('#id_national_code').val() : '';
 | 
				
			||||||
 | 
					      formData.append('national_code', (ncField || $('#rep_national_code').val().trim()));
 | 
				
			||||||
 | 
					      formData.append('first_name', $('#id_first_name').val() || '');
 | 
				
			||||||
 | 
					      formData.append('last_name', $('#id_last_name').val() || '');
 | 
				
			||||||
 | 
					      formData.append('phone_number_1', $('#id_phone_number_1').val() || '');
 | 
				
			||||||
 | 
					      formData.append('phone_number_2', $('#id_phone_number_2').val() || '');
 | 
				
			||||||
 | 
					      formData.append('card_number', $('#id_card_number').val() || '');
 | 
				
			||||||
 | 
					      formData.append('account_number', $('#id_account_number').val() || '');
 | 
				
			||||||
 | 
					      formData.append('address', $('#id_address').val() || '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Include WellForm fields so edits are saved
 | 
				
			||||||
 | 
					      if ($('#wellFormBlock').is(':visible')) {
 | 
				
			||||||
 | 
					        formData.append('electricity_subscription_number', $('#id_electricity_subscription_number').val() || '');
 | 
				
			||||||
 | 
					        formData.append('water_meter_serial_number', $('#id_water_meter_serial_number').val() || '');
 | 
				
			||||||
 | 
					        formData.append('water_meter_old_serial_number', $('#id_water_meter_old_serial_number').val() || '');
 | 
				
			||||||
 | 
					        formData.append('water_meter_manufacturer', $('#id_water_meter_manufacturer').is(':visible') ? ($('#id_water_meter_manufacturer').val() || '') : '');
 | 
				
			||||||
 | 
					        formData.append('new_manufacturer', $('#id_new_manufacturer').is(':visible') ? ($('#id_new_manufacturer').val() || '') : '');
 | 
				
			||||||
 | 
					        formData.append('utm_x', $('#id_utm_x').val() || '');
 | 
				
			||||||
 | 
					        formData.append('utm_y', $('#id_utm_y').val() || '');
 | 
				
			||||||
 | 
					        formData.append('utm_zone', $('#id_utm_zone').val() || '');
 | 
				
			||||||
 | 
					        formData.append('utm_hemisphere', $('#id_utm_hemisphere').val() || '');
 | 
				
			||||||
 | 
					        formData.append('well_power', $('#id_well_power').val() || '');
 | 
				
			||||||
 | 
					        formData.append('reference_letter_number', $('#id_reference_letter_number').val() || '');
 | 
				
			||||||
 | 
					        // Use gregorian date if available, otherwise use the field value
 | 
				
			||||||
 | 
					        const gregorianDate = $('#id_reference_letter_date').attr('data-gregorian');
 | 
				
			||||||
 | 
					        formData.append('reference_letter_date', gregorianDate || $('#id_reference_letter_date').val() || '');
 | 
				
			||||||
 | 
					        // Remove flag
 | 
				
			||||||
 | 
					        formData.append('remove_file', $('#remove-file').val() || 'false');
 | 
				
			||||||
 | 
					        const repFile = document.getElementById('id_representative_letter_file');
 | 
				
			||||||
 | 
					        if (repFile && repFile.files && repFile.files[0]) {
 | 
				
			||||||
 | 
					          formData.append('representative_letter_file', repFile.files[0]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const $btn = $(this).prop('disabled', true).text('در حال ذخیره...');
 | 
				
			||||||
 | 
					      $.ajax({
 | 
				
			||||||
 | 
					        url: '{% url "processes:create_request_with_entities" %}',
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        data: formData,
 | 
				
			||||||
 | 
					        processData: false,
 | 
				
			||||||
 | 
					        contentType: false,
 | 
				
			||||||
 | 
					      }).done(function(resp){
 | 
				
			||||||
 | 
					        if (resp.ok) {
 | 
				
			||||||
 | 
					          showToast('درخواست با موفقیت ثبت شد', 'success');
 | 
				
			||||||
 | 
					          if (resp.redirect) {
 | 
				
			||||||
 | 
					            setTimeout(function(){ window.location.href = resp.redirect; }, 800);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            setTimeout(function(){ location.reload(); }, 1200);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const msg = buildErrorMessage(resp);
 | 
				
			||||||
 | 
					          showToast(msg, 'danger');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }).fail(function(xhr){
 | 
				
			||||||
 | 
					        let msg = 'خطا در ذخیره';
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const resp = JSON.parse(xhr.responseText);
 | 
				
			||||||
 | 
					          msg = buildErrorMessage(resp) || msg;
 | 
				
			||||||
 | 
					        } catch(e) {}
 | 
				
			||||||
 | 
					        showToast(msg, 'danger');
 | 
				
			||||||
 | 
					      }).always(function(){
 | 
				
			||||||
 | 
					        $btn.prop('disabled', false).text('ذخیره');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function buildErrorMessage(resp){
 | 
				
			||||||
 | 
					      if (!resp) return '';
 | 
				
			||||||
 | 
					      if (resp.error) return resp.error;
 | 
				
			||||||
 | 
					      if (resp.errors) {
 | 
				
			||||||
 | 
					        // Collect form-related errors
 | 
				
			||||||
 | 
					        const parts = [];
 | 
				
			||||||
 | 
					        if (resp.errors.customer) {
 | 
				
			||||||
 | 
					          parts.push('خطای نماینده: ' + flattenErrors(resp.errors.customer));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (resp.errors.well) {
 | 
				
			||||||
 | 
					          parts.push('خطای چاه: ' + flattenErrors(resp.errors.well));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return parts.join(' | ');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function flattenErrors(errorsObj){
 | 
				
			||||||
 | 
					      if (typeof errorsObj === 'string') return errorsObj;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const parts = [];
 | 
				
			||||||
 | 
					        for (const k in errorsObj){
 | 
				
			||||||
 | 
					          const v = errorsObj[k];
 | 
				
			||||||
 | 
					          if (Array.isArray(v)) parts.push(`${k}: ${v[0]}`);
 | 
				
			||||||
 | 
					          else if (typeof v === 'string') parts.push(`${k}: ${v}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return parts.join('، ');
 | 
				
			||||||
 | 
					      } catch(e){
 | 
				
			||||||
 | 
					        return '';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#btnToggleManufacturer').on('click', function() {
 | 
				
			||||||
 | 
					      const $select = $('#id_water_meter_manufacturer');
 | 
				
			||||||
 | 
					      const $input = $('#id_new_manufacturer');
 | 
				
			||||||
 | 
					      const $btn = $(this);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if ($select.is(':visible')) {
 | 
				
			||||||
 | 
					        $select.hide();
 | 
				
			||||||
 | 
					        $input.show().focus();
 | 
				
			||||||
 | 
					        $btn.html('<i class="bx bx-check"></i>');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        $input.hide();
 | 
				
			||||||
 | 
					        $select.show();
 | 
				
			||||||
 | 
					        $btn.html('<i class="bx bx-plus"></i>');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#requestModal').on('hidden.bs.modal', function(){
 | 
				
			||||||
 | 
					      $('#requestForm')[0].reset();
 | 
				
			||||||
 | 
					      $('#wellFormBlock').hide();
 | 
				
			||||||
 | 
					      $('#repNewFields').hide();
 | 
				
			||||||
 | 
					      $('#id_reference_letter_date').removeAttr('data-gregorian');
 | 
				
			||||||
 | 
					      // Reset file UI
 | 
				
			||||||
 | 
					      $('#current-file-display').hide();
 | 
				
			||||||
 | 
					      $('#id_representative_letter_file').show().val('');
 | 
				
			||||||
 | 
					      $('#remove-file').val('false');
 | 
				
			||||||
 | 
					      setStatus('#wellStatus', '', '');
 | 
				
			||||||
 | 
					      setStatus('#repStatus', '', '');
 | 
				
			||||||
 | 
					      currentWellId = null;
 | 
				
			||||||
 | 
					      currentRepId = null;
 | 
				
			||||||
 | 
					      wellChecked = false;
 | 
				
			||||||
 | 
					      repChecked = false;
 | 
				
			||||||
 | 
					      checkSaveButton();
 | 
				
			||||||
 | 
					      clearInlineErrors(); // Clear inline errors on modal close
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle selecting a new file: hide existing file display and cancel removal flag
 | 
				
			||||||
 | 
					    $('#id_representative_letter_file').on('change', function() {
 | 
				
			||||||
 | 
					      if (this.files && this.files.length > 0) {
 | 
				
			||||||
 | 
					        $('#current-file-display').hide();
 | 
				
			||||||
 | 
					        $('#remove-file').val('false');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Expose remove function
 | 
				
			||||||
 | 
					    window.removeCurrentFile = function() {
 | 
				
			||||||
 | 
					      $('#current-file-display').hide();
 | 
				
			||||||
 | 
					      $('#remove-file').val('true');
 | 
				
			||||||
 | 
					      $('#id_representative_letter_file').show().val('');
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Delete request function
 | 
				
			||||||
 | 
					    window.deleteRequest = function(instanceId, instanceCode) {
 | 
				
			||||||
 | 
					      // Set modal content
 | 
				
			||||||
 | 
					      document.getElementById('deleteConfirmText').textContent = `آیا از حذف درخواست ${instanceCode} اطمینان دارید؟`;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Show modal
 | 
				
			||||||
 | 
					      const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
 | 
				
			||||||
 | 
					      modal.show();
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Handle confirm button click
 | 
				
			||||||
 | 
					      document.getElementById('confirmDeleteBtn').onclick = function() {
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					          url: '{% url "processes:delete_request" 0 %}'.replace('0', instanceId),
 | 
				
			||||||
 | 
					          type: 'POST',
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          success: function(response) {
 | 
				
			||||||
 | 
					            if (response.success) {
 | 
				
			||||||
 | 
					              showToast(response.message, 'success');
 | 
				
			||||||
 | 
					              modal.hide();
 | 
				
			||||||
 | 
					              setTimeout(() => {
 | 
				
			||||||
 | 
					                window.location.reload();
 | 
				
			||||||
 | 
					              }, 1500);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              showToast(response.message, 'danger');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          error: function() {
 | 
				
			||||||
 | 
					            showToast('خطا در ارتباط با سرور', 'danger');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										97
									
								
								processes/templates/processes/step_detail.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								processes/templates/processes/step_detail.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,97 @@
 | 
				
			||||||
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load processes_tags %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block sidebar %}
 | 
				
			||||||
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock sidebar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block navbar %}
 | 
				
			||||||
 | 
					    {% include 'navbars/admin.html' %}
 | 
				
			||||||
 | 
					{% endblock navbar %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{{ step.name }} - درخواست {{ instance.code }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block style %}
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% include '_toasts.html' %}
 | 
				
			||||||
 | 
					<div class="container-xxl flex-grow-1 container-p-y">
 | 
				
			||||||
 | 
					  <div class="row">
 | 
				
			||||||
 | 
					    <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					      <div class="d-flex align-items-center justify-content-between mb-3">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
 | 
				
			||||||
 | 
					          <small class="text-muted d-block">
 | 
				
			||||||
 | 
					            اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
 | 
				
			||||||
 | 
					            | نماینده: {{ instance.representative.profile.national_code|default:"-" }}
 | 
				
			||||||
 | 
					          </small>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
				
			||||||
 | 
					        {% stepper_header instance step %}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="bs-stepper-content">
 | 
				
			||||||
 | 
					          <div class="content active dstepper-block">
 | 
				
			||||||
 | 
					            <div class="content-header mb-3">
 | 
				
			||||||
 | 
					              <h6 class="mb-0">{{ step.name }}</h6>
 | 
				
			||||||
 | 
					              <small>{{ step.description|default:' ' }}</small>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div class="row g-3">
 | 
				
			||||||
 | 
					              <div class="col-12">
 | 
				
			||||||
 | 
					                <div class="alert alert-info">
 | 
				
			||||||
 | 
					                  <h6>وضعیت مرحله: 
 | 
				
			||||||
 | 
					                    {% if step_instance %}
 | 
				
			||||||
 | 
					                      {{ step_instance.get_status_display_with_color|safe }}
 | 
				
			||||||
 | 
					                    {% else %}
 | 
				
			||||||
 | 
					                      <span class="badge bg-secondary">در انتظار</span>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </h6>
 | 
				
			||||||
 | 
					                  <p class="mb-0">فرم این مرحله بعداً پیادهسازی میشود.</p>
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
 | 
					                  {% if step_instance and step_instance.notes %}
 | 
				
			||||||
 | 
					                    <hr>
 | 
				
			||||||
 | 
					                    <strong>یادداشتها:</strong>
 | 
				
			||||||
 | 
					                    <p class="mb-0">{{ step_instance.notes }}</p>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="col-12 d-flex justify-content-between">
 | 
				
			||||||
 | 
					                {% if previous_step %}
 | 
				
			||||||
 | 
					                  <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" 
 | 
				
			||||||
 | 
					                     class="btn btn-label-secondary">
 | 
				
			||||||
 | 
					                    <i class="bx bx-chevron-left bx-sm ms-sm-n2"></i>
 | 
				
			||||||
 | 
					                    <span class="align-middle d-sm-inline-block d-none">قبلی</span>
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                  <span></span>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                {% if next_step %}
 | 
				
			||||||
 | 
					                  <a href="{% url 'processes:step_detail' instance.id next_step.id %}" 
 | 
				
			||||||
 | 
					                     class="btn btn-primary">
 | 
				
			||||||
 | 
					                    <span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
 | 
				
			||||||
 | 
					                    <i class="bx bx-chevron-right bx-sm me-sm-n2"></i>
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                  <button class="btn btn-success" type="button">اتمام</button>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block script %}
 | 
				
			||||||
 | 
					<script src="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.js' %}"></script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										0
									
								
								processes/templatetags/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								processes/templatetags/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										48
									
								
								processes/templatetags/processes_tags.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								processes/templatetags/processes_tags.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					from django import template
 | 
				
			||||||
 | 
					from django.utils.safestring import mark_safe
 | 
				
			||||||
 | 
					from ..models import ProcessInstance, StepInstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					register = template.Library()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.filter(name='get_item')
 | 
				
			||||||
 | 
					def get_item(mapping, key):
 | 
				
			||||||
 | 
						try:
 | 
				
			||||||
 | 
							return mapping.get(key)
 | 
				
			||||||
 | 
						except Exception:
 | 
				
			||||||
 | 
							return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.inclusion_tag('processes/includes/stepper_header.html')
 | 
				
			||||||
 | 
					def stepper_header(instance, current_step=None):
 | 
				
			||||||
 | 
					    """رندر کردن header مراحل برای instance"""
 | 
				
			||||||
 | 
					    if not isinstance(instance, ProcessInstance):
 | 
				
			||||||
 | 
					        return {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    steps = list(instance.process.steps.all().order_by('order'))
 | 
				
			||||||
 | 
					    step_instances = instance.step_instances.select_related('step').all()
 | 
				
			||||||
 | 
					    step_id_to_status = {si.step_id: si.status for si in step_instances}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    steps_context = []
 | 
				
			||||||
 | 
					    for step in steps:
 | 
				
			||||||
 | 
					        step_instance = next((si for si in step_instances if si.step_id == step.id), None)
 | 
				
			||||||
 | 
					        status = step_id_to_status.get(step.id, 'pending')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # بررسی دسترسی به مرحله
 | 
				
			||||||
 | 
					        can_access = instance.can_access_step(step)
 | 
				
			||||||
 | 
					        # مرحله انتخابشده (نمایش فعلی)
 | 
				
			||||||
 | 
					        is_selected = bool(current_step and step.id == current_step.id)
 | 
				
			||||||
 | 
					        # مرحلهای که باید انجام شود (مرحله جاری در instance)
 | 
				
			||||||
 | 
					        is_todo = bool(instance.current_step and step.id == instance.current_step.id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        steps_context.append({
 | 
				
			||||||
 | 
					            'step': step,
 | 
				
			||||||
 | 
					            'status': status,
 | 
				
			||||||
 | 
					            'can_access': can_access,
 | 
				
			||||||
 | 
					            'is_selected': is_selected,
 | 
				
			||||||
 | 
					            'is_todo': is_todo,
 | 
				
			||||||
 | 
					            'step_instance': step_instance,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'steps_context': steps_context,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,18 @@ from . import views
 | 
				
			||||||
app_name = 'processes'
 | 
					app_name = 'processes'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
 | 
					    # Requests UI
 | 
				
			||||||
 | 
					    path('requests/', views.request_list, name='request_list'),
 | 
				
			||||||
 | 
					    path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'),
 | 
				
			||||||
 | 
					    path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'),
 | 
				
			||||||
 | 
					    path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'),
 | 
				
			||||||
 | 
					    path('requests/<int:instance_id>/delete/', views.delete_request, name='delete_request'),
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # New step-based architecture
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/steps/', views.instance_steps, name='instance_steps'),
 | 
				
			||||||
 | 
					    path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Legacy process views
 | 
				
			||||||
    path('', views.process_list, name='process_list'),
 | 
					    path('', views.process_list, name='process_list'),
 | 
				
			||||||
    path('<int:process_id>/', views.process_detail, name='process_detail'),
 | 
					    path('<int:process_id>/', views.process_detail, name='process_detail'),
 | 
				
			||||||
    path('<int:process_id>/start/', views.start_process, name='start_process'),
 | 
					    path('<int:process_id>/start/', views.start_process, name='start_process'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,21 @@
 | 
				
			||||||
from django.shortcuts import render, get_object_or_404, redirect
 | 
					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.auth.decorators import login_required
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.http import JsonResponse
 | 
					from django.http import JsonResponse
 | 
				
			||||||
from django.views.decorators.http import require_POST
 | 
					from django.views.decorators.http import require_POST, require_GET
 | 
				
			||||||
 | 
					from django.db import transaction
 | 
				
			||||||
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from .models import Process, ProcessInstance, StepInstance
 | 
					from .models import Process, ProcessInstance, StepInstance
 | 
				
			||||||
 | 
					from wells.models import Well
 | 
				
			||||||
 | 
					from accounts.models import Profile
 | 
				
			||||||
from .forms import ProcessInstanceForm
 | 
					from .forms import ProcessInstanceForm
 | 
				
			||||||
 | 
					from accounts.forms import CustomerForm
 | 
				
			||||||
 | 
					from wells.forms import WellForm
 | 
				
			||||||
 | 
					from wells.models import WaterMeterManufacturer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def process_list(request):
 | 
					def process_list(request):
 | 
				
			||||||
| 
						 | 
					@ -22,6 +33,272 @@ def process_detail(request, process_id):
 | 
				
			||||||
        'process': process
 | 
					        'process': process
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def request_list(request):
 | 
				
			||||||
 | 
					    """نمایش لیست درخواستها با جدول و مدال ایجاد"""
 | 
				
			||||||
 | 
					    instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').filter(is_deleted=False).order_by('-created')
 | 
				
			||||||
 | 
					    processes = Process.objects.filter(is_active=True)
 | 
				
			||||||
 | 
					    manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
 | 
				
			||||||
 | 
					    return render(request, 'processes/request_list.html', {
 | 
				
			||||||
 | 
					        'instances': instances,
 | 
				
			||||||
 | 
					        'customer_form': CustomerForm(),
 | 
				
			||||||
 | 
					        'well_form': WellForm(),
 | 
				
			||||||
 | 
					        'processes': processes,
 | 
				
			||||||
 | 
					        'manufacturers': manufacturers
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_GET
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def lookup_well_by_subscription(request):
 | 
				
			||||||
 | 
					    sub = request.GET.get('water_subscription_number', '').strip()
 | 
				
			||||||
 | 
					    if not sub:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'error': 'شماره اشتراک الزامی است'}, status=400)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        well = Well.objects.select_related('representative', 'water_meter_manufacturer').get(water_subscription_number=sub)
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            'id': well.id,
 | 
				
			||||||
 | 
					            'water_subscription_number': well.water_subscription_number,
 | 
				
			||||||
 | 
					            'electricity_subscription_number': well.electricity_subscription_number,
 | 
				
			||||||
 | 
					            'water_meter_serial_number': well.water_meter_serial_number,
 | 
				
			||||||
 | 
					            'water_meter_old_serial_number': well.water_meter_old_serial_number,
 | 
				
			||||||
 | 
					            'water_meter_manufacturer': well.water_meter_manufacturer.id if well.water_meter_manufacturer else None,
 | 
				
			||||||
 | 
					            'utm_x': str(well.utm_x) if well.utm_x is not None else None,
 | 
				
			||||||
 | 
					            'utm_y': str(well.utm_y) if well.utm_y is not None else None,
 | 
				
			||||||
 | 
					            'utm_zone': well.utm_zone,
 | 
				
			||||||
 | 
					            'utm_hemisphere': well.utm_hemisphere,
 | 
				
			||||||
 | 
					            'well_power': well.well_power,
 | 
				
			||||||
 | 
					            'reference_letter_number': well.reference_letter_number,
 | 
				
			||||||
 | 
					            'reference_letter_date': well.reference_letter_date.isoformat() if well.reference_letter_date else None,
 | 
				
			||||||
 | 
					            'representative_letter_file_url': well.representative_letter_file.url if well.representative_letter_file else '',
 | 
				
			||||||
 | 
					            'representative_letter_file_name': well.representative_letter_file.name.split('/')[-1] if well.representative_letter_file else '',
 | 
				
			||||||
 | 
					            'representative_id': well.representative.id if well.representative else None,
 | 
				
			||||||
 | 
					            'representative_full_name': well.representative.get_full_name() if well.representative else None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': True, 'exists': True, 'well': data})
 | 
				
			||||||
 | 
					    except Well.DoesNotExist:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': True, 'exists': False})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_GET
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def lookup_representative_by_national_code(request):
 | 
				
			||||||
 | 
					    national_code = request.GET.get('national_code', '').strip()
 | 
				
			||||||
 | 
					    if not national_code:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'error': 'کد ملی الزامی است'}, status=400)
 | 
				
			||||||
 | 
					    profile = Profile.objects.select_related('user').filter(national_code=national_code).first()
 | 
				
			||||||
 | 
					    if not profile:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': True, 'exists': False})
 | 
				
			||||||
 | 
					    user = profile.user
 | 
				
			||||||
 | 
					    return JsonResponse({
 | 
				
			||||||
 | 
					        'ok': True,
 | 
				
			||||||
 | 
					        'exists': True,
 | 
				
			||||||
 | 
					        'user': {
 | 
				
			||||||
 | 
					            'id': user.id,
 | 
				
			||||||
 | 
					            'username': user.username,
 | 
				
			||||||
 | 
					            'first_name': user.first_name,
 | 
				
			||||||
 | 
					            'last_name': user.last_name,
 | 
				
			||||||
 | 
					            'full_name': user.get_full_name(),
 | 
				
			||||||
 | 
					            'profile': {
 | 
				
			||||||
 | 
					                'national_code': profile.national_code,
 | 
				
			||||||
 | 
					                'phone_number_1': profile.phone_number_1,
 | 
				
			||||||
 | 
					                'phone_number_2': profile.phone_number_2,
 | 
				
			||||||
 | 
					                'card_number': profile.card_number,
 | 
				
			||||||
 | 
					                'account_number': profile.account_number,
 | 
				
			||||||
 | 
					                'address': profile.address,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					@transaction.atomic
 | 
				
			||||||
 | 
					def create_request_with_entities(request):
 | 
				
			||||||
 | 
					    """ایجاد/بهروزرسانی چاه و نماینده و سپس ایجاد درخواست"""
 | 
				
			||||||
 | 
					    User = get_user_model()
 | 
				
			||||||
 | 
					    process_id = request.POST.get('process')
 | 
				
			||||||
 | 
					    process = Process.objects.get(id=process_id)
 | 
				
			||||||
 | 
					    description = request.POST.get('description', '')
 | 
				
			||||||
 | 
					    # Well fields
 | 
				
			||||||
 | 
					    water_subscription_number = request.POST.get('water_subscription_number')
 | 
				
			||||||
 | 
					    well_id = request.POST.get('well_id')  # optional if existing
 | 
				
			||||||
 | 
					    # Representative fields
 | 
				
			||||||
 | 
					    representative_id = request.POST.get('representative_id')
 | 
				
			||||||
 | 
					    # Prefer plain CustomerForm keys; fallback to representative_* keys
 | 
				
			||||||
 | 
					    representative_national_code = request.POST.get('national_code') or request.POST.get('representative_national_code')
 | 
				
			||||||
 | 
					    representative_first_name = request.POST.get('first_name') or request.POST.get('representative_first_name')
 | 
				
			||||||
 | 
					    representative_last_name = request.POST.get('last_name') or request.POST.get('representative_last_name')
 | 
				
			||||||
 | 
					    representative_username = request.POST.get('username') or request.POST.get('representative_username')
 | 
				
			||||||
 | 
					    representative_phone_number_1 = request.POST.get('phone_number_1') or request.POST.get('representative_phone_number_1')
 | 
				
			||||||
 | 
					    representative_phone_number_2 = request.POST.get('phone_number_2') or request.POST.get('representative_phone_number_2')
 | 
				
			||||||
 | 
					    representative_card_number = request.POST.get('card_number') or request.POST.get('representative_card_number')
 | 
				
			||||||
 | 
					    representative_account_number = request.POST.get('account_number') or request.POST.get('representative_account_number')
 | 
				
			||||||
 | 
					    representative_address = request.POST.get('address') or request.POST.get('representative_address')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not process_id:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'errors': {'request': {'process': ['فرآیند الزامی است']}}}, status=400)
 | 
				
			||||||
 | 
					    if not water_subscription_number:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'errors': {'well': {'water_subscription_number': ['شماره اشتراک آب الزامی است']}}}, status=400)
 | 
				
			||||||
 | 
					    if not representative_id and not representative_national_code:
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    representative_user = None
 | 
				
			||||||
 | 
					    representative_profile = None
 | 
				
			||||||
 | 
					    if representative_id:
 | 
				
			||||||
 | 
					        representative_profile = Profile.objects.select_related('user').filter(user_id=representative_id).first()
 | 
				
			||||||
 | 
					        if not representative_profile:
 | 
				
			||||||
 | 
					            return JsonResponse({'ok': False, 'errors': {'customer': {'__all__': ['نماینده انتخابشده یافت نشد']}}}, status=400)
 | 
				
			||||||
 | 
					        representative_user = representative_profile.user
 | 
				
			||||||
 | 
					        # Optionally update if fields provided
 | 
				
			||||||
 | 
					        changed = False
 | 
				
			||||||
 | 
					        if representative_first_name:
 | 
				
			||||||
 | 
					            representative_user.first_name = representative_first_name
 | 
				
			||||||
 | 
					            changed = True
 | 
				
			||||||
 | 
					        if representative_last_name:
 | 
				
			||||||
 | 
					            representative_user.last_name = representative_last_name
 | 
				
			||||||
 | 
					            changed = True
 | 
				
			||||||
 | 
					        if representative_username:
 | 
				
			||||||
 | 
					            representative_user.username = representative_username
 | 
				
			||||||
 | 
					            changed = True
 | 
				
			||||||
 | 
					        if changed:
 | 
				
			||||||
 | 
					            representative_user.save()
 | 
				
			||||||
 | 
					        if representative_national_code:
 | 
				
			||||||
 | 
					            representative_profile.national_code = representative_national_code
 | 
				
			||||||
 | 
					        if representative_phone_number_1 is not None:
 | 
				
			||||||
 | 
					            representative_profile.phone_number_1 = representative_phone_number_1
 | 
				
			||||||
 | 
					        if representative_phone_number_2 is not None:
 | 
				
			||||||
 | 
					            representative_profile.phone_number_2 = representative_phone_number_2
 | 
				
			||||||
 | 
					        if representative_card_number is not None:
 | 
				
			||||||
 | 
					            representative_profile.card_number = representative_card_number
 | 
				
			||||||
 | 
					        if representative_account_number is not None:
 | 
				
			||||||
 | 
					            representative_profile.account_number = representative_account_number
 | 
				
			||||||
 | 
					        if representative_address is not None:
 | 
				
			||||||
 | 
					            representative_profile.address = representative_address
 | 
				
			||||||
 | 
					        representative_profile.save()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # Use CustomerForm to validate/create/update representative profile by national code
 | 
				
			||||||
 | 
					        profile_instance = None
 | 
				
			||||||
 | 
					        if representative_national_code:
 | 
				
			||||||
 | 
					            profile_instance = Profile.objects.filter(national_code=representative_national_code).first()
 | 
				
			||||||
 | 
					        customer_data = {
 | 
				
			||||||
 | 
					            'first_name': representative_first_name or '',
 | 
				
			||||||
 | 
					            'last_name': representative_last_name or '',
 | 
				
			||||||
 | 
					            'phone_number_1': representative_phone_number_1 or '',
 | 
				
			||||||
 | 
					            'phone_number_2': representative_phone_number_2 or '',
 | 
				
			||||||
 | 
					            'national_code': representative_national_code or '',
 | 
				
			||||||
 | 
					            'address': representative_address or '',
 | 
				
			||||||
 | 
					            'card_number': representative_card_number or '',
 | 
				
			||||||
 | 
					            'account_number': representative_account_number or '',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        customer_form = CustomerForm(customer_data, instance=profile_instance)
 | 
				
			||||||
 | 
					        customer_form.request = request
 | 
				
			||||||
 | 
					        if not customer_form.is_valid():
 | 
				
			||||||
 | 
					            return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
 | 
				
			||||||
 | 
					        representative_profile = customer_form.save()
 | 
				
			||||||
 | 
					        representative_user = representative_profile.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Resolve/create/update well
 | 
				
			||||||
 | 
					    # Build WellForm data from POST
 | 
				
			||||||
 | 
					    well = None
 | 
				
			||||||
 | 
					    if well_id:
 | 
				
			||||||
 | 
					        well = Well.objects.filter(id=well_id).first()
 | 
				
			||||||
 | 
					        if not well:
 | 
				
			||||||
 | 
					            return JsonResponse({'ok': False, 'error': 'شناسه چاه نامعتبر است'}, status=400)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        existing = Well.objects.filter(water_subscription_number=water_subscription_number).first()
 | 
				
			||||||
 | 
					        if existing:
 | 
				
			||||||
 | 
					            well = existing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    well_data = request.POST.copy()
 | 
				
			||||||
 | 
					    # Ensure representative set from created/selected user if not provided
 | 
				
			||||||
 | 
					    if representative_user and not well_data.get('representative'):
 | 
				
			||||||
 | 
					        well_data['representative'] = str(representative_user.id)
 | 
				
			||||||
 | 
					    if not well_data.get('water_subscription_number'):
 | 
				
			||||||
 | 
					        well_data['water_subscription_number'] = water_subscription_number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Preserve existing values on partial updates
 | 
				
			||||||
 | 
					    if well:
 | 
				
			||||||
 | 
					        for field_name in WellForm.Meta.fields:
 | 
				
			||||||
 | 
					            if field_name in ('representative_letter_file',):
 | 
				
			||||||
 | 
					                # File field handled via request.FILES; skip if not provided
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            incoming = well_data.get(field_name, None)
 | 
				
			||||||
 | 
					            if incoming is None or incoming == '':
 | 
				
			||||||
 | 
					                current_value = getattr(well, field_name, None)
 | 
				
			||||||
 | 
					                if current_value is None:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                # Convert FK to id
 | 
				
			||||||
 | 
					                if hasattr(current_value, 'pk'):
 | 
				
			||||||
 | 
					                    well_data[field_name] = str(current_value.pk)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    # Convert dates/decimals/others to string
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        well_data[field_name] = current_value.isoformat()  # dates
 | 
				
			||||||
 | 
					                    except AttributeError:
 | 
				
			||||||
 | 
					                        well_data[field_name] = str(current_value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    well_form = WellForm(well_data, request.FILES, instance=well)
 | 
				
			||||||
 | 
					    if not well_form.is_valid():
 | 
				
			||||||
 | 
					        return JsonResponse({'ok': False, 'errors': {'well': well_form.errors}}, status=400)
 | 
				
			||||||
 | 
					    # Save with ability to remove existing file
 | 
				
			||||||
 | 
					    well = well_form.save(commit=False)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        if request.POST.get('remove_file') == 'true' and getattr(well, 'representative_letter_file', None):
 | 
				
			||||||
 | 
					            well.representative_letter_file.delete(save=False)
 | 
				
			||||||
 | 
					            well.representative_letter_file = None
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    well.save()
 | 
				
			||||||
 | 
					    # Auto fill geo ownership from current user profile if available
 | 
				
			||||||
 | 
					    current_profile = getattr(request.user, 'profile', None)
 | 
				
			||||||
 | 
					    if current_profile:
 | 
				
			||||||
 | 
					        if hasattr(well, 'affairs'):
 | 
				
			||||||
 | 
					            well.affairs = current_profile.affairs
 | 
				
			||||||
 | 
					        if hasattr(well, 'county'):
 | 
				
			||||||
 | 
					            well.county = current_profile.county
 | 
				
			||||||
 | 
					        if hasattr(well, 'broker'):
 | 
				
			||||||
 | 
					            well.broker = current_profile.broker
 | 
				
			||||||
 | 
					        well.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create request instance
 | 
				
			||||||
 | 
					    instance = ProcessInstance.objects.create(
 | 
				
			||||||
 | 
					        process=process,
 | 
				
			||||||
 | 
					        description=description,
 | 
				
			||||||
 | 
					        well=well,
 | 
				
			||||||
 | 
					        representative=representative_user,
 | 
				
			||||||
 | 
					        requester=request.user,
 | 
				
			||||||
 | 
					        status='pending',
 | 
				
			||||||
 | 
					        priority='medium',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    # ایجاد نمونههای مرحله بر اساس مراحل فرآیند و تنظیم مرحله فعلی
 | 
				
			||||||
 | 
					    for step in process.steps.all().order_by('order'):
 | 
				
			||||||
 | 
					        StepInstance.objects.create(
 | 
				
			||||||
 | 
					            process_instance=instance,
 | 
				
			||||||
 | 
					            step=step
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    first_step = process.steps.all().order_by('order').first()
 | 
				
			||||||
 | 
					    if first_step:
 | 
				
			||||||
 | 
					        instance.current_step = first_step
 | 
				
			||||||
 | 
					        instance.status = 'in_progress'
 | 
				
			||||||
 | 
					        instance.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect_url = reverse('processes:instance_steps', args=[instance.id])
 | 
				
			||||||
 | 
					    return JsonResponse({'ok': True, 'instance_id': instance.id, 'redirect': redirect_url})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@require_POST
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def delete_request(request, instance_id):
 | 
				
			||||||
 | 
					    """حذف درخواست"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    code = instance.code
 | 
				
			||||||
 | 
					    instance.delete()
 | 
				
			||||||
 | 
					    return JsonResponse({
 | 
				
			||||||
 | 
					        'success': True,
 | 
				
			||||||
 | 
					        'message': f'درخواست {code} با موفقیت حذف شد'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def start_process(request, process_id):
 | 
					def start_process(request, process_id):
 | 
				
			||||||
    """شروع فرآیند جدید"""
 | 
					    """شروع فرآیند جدید"""
 | 
				
			||||||
| 
						 | 
					@ -67,6 +344,61 @@ def instance_detail(request, instance_id):
 | 
				
			||||||
        'instance': instance
 | 
					        'instance': instance
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def step_detail(request, instance_id, step_id):
 | 
				
			||||||
 | 
					    """نمایش جزئیات مرحله خاص"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(
 | 
				
			||||||
 | 
					        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
				
			||||||
 | 
					        id=instance_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # بررسی دسترسی به مرحله
 | 
				
			||||||
 | 
					    if not instance.can_access_step(step):
 | 
				
			||||||
 | 
					        messages.error(request, 'شما به این مرحله دسترسی ندارید. ابتدا مراحل قبلی را تکمیل کنید.')
 | 
				
			||||||
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # هدایت به view مناسب بر اساس نوع مرحله
 | 
				
			||||||
 | 
					    if step.order == 1:  # مرحله اول - انتخاب اقلام
 | 
				
			||||||
 | 
					        return redirect('invoices:quote_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					    elif step.order == 2:  # مرحله دوم - صدور پیشفاکتور
 | 
				
			||||||
 | 
					        return redirect('invoices:quote_preview_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					    elif step.order == 3:  # مرحله سوم - ثبت فیشهای واریزی
 | 
				
			||||||
 | 
					        return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # برای سایر مراحل، template عمومی نمایش داده میشود
 | 
				
			||||||
 | 
					    step_instance = instance.step_instances.filter(step=step).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Navigation logic
 | 
				
			||||||
 | 
					    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
				
			||||||
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return render(request, 'processes/step_detail.html', {
 | 
				
			||||||
 | 
					        'instance': instance,
 | 
				
			||||||
 | 
					        'step': step,
 | 
				
			||||||
 | 
					        'step_instance': step_instance,
 | 
				
			||||||
 | 
					        'previous_step': previous_step,
 | 
				
			||||||
 | 
					        'next_step': next_step,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required 
 | 
				
			||||||
 | 
					def instance_steps(request, instance_id):
 | 
				
			||||||
 | 
					    """هدایت به مرحله فعلی instance"""
 | 
				
			||||||
 | 
					    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not instance.current_step:
 | 
				
			||||||
 | 
					        # اگر مرحله فعلی تعریف نشده، به اولین مرحله برو
 | 
				
			||||||
 | 
					        first_step = instance.process.steps.first()
 | 
				
			||||||
 | 
					        if first_step:
 | 
				
			||||||
 | 
					            instance.current_step = first_step
 | 
				
			||||||
 | 
					            instance.save()
 | 
				
			||||||
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=first_step.id)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            messages.error(request, 'هیچ مرحلهای برای این فرآیند تعریف نشده است.')
 | 
				
			||||||
 | 
					            return redirect('processes:request_list')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def my_processes(request):
 | 
					def my_processes(request):
 | 
				
			||||||
    """نمایش فرآیندهای کاربر"""
 | 
					    """نمایش فرآیندهای کاربر"""
 | 
				
			||||||
| 
						 | 
					@ -77,3 +409,4 @@ def my_processes(request):
 | 
				
			||||||
        'my_instances': my_instances,
 | 
					        'my_instances': my_instances,
 | 
				
			||||||
        'assigned_steps': assigned_steps
 | 
					        'assigned_steps': assigned_steps
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,6 +98,18 @@
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- requests -->
 | 
				
			||||||
 | 
					    <li class="menu-header small text-uppercase">
 | 
				
			||||||
 | 
					      <span class="menu-header-text">درخواستها</span>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Users -->
 | 
				
			||||||
 | 
					    <li class="menu-item {% if 'processes' in request.path or 'step' in request.path %}active{% endif %}">
 | 
				
			||||||
 | 
					      <a href="{% url 'processes:request_list' %}" class="menu-link">
 | 
				
			||||||
 | 
					        <i class="menu-icon tf-icons bx bx-user"></i>
 | 
				
			||||||
 | 
					        <div class="text-truncate">درخواستها</div>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Customers -->
 | 
					    <!-- Customers -->
 | 
				
			||||||
    <li class="menu-header small text-uppercase">
 | 
					    <li class="menu-header small text-uppercase">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,8 @@ class WellForm(forms.ModelForm):
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'electricity_subscription_number': forms.TextInput(attrs={
 | 
					            'electricity_subscription_number': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': 'شماره اشتراک برق'
 | 
					                'placeholder': 'شماره اشتراک برق',
 | 
				
			||||||
 | 
					                'required': True
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'water_meter_serial_number': forms.TextInput(attrs={
 | 
					            'water_meter_serial_number': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 09:08
 | 
					# Generated by Django 5.2.4 on 2025-08-14 09:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
import simple_history.models
 | 
					import simple_history.models
 | 
				
			||||||
 | 
					import wells.models
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,10 +12,27 @@ class Migration(migrations.Migration):
 | 
				
			||||||
    initial = True
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('locations', '0001_initial'),
 | 
				
			||||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='WaterMeterManufacturer',
 | 
				
			||||||
 | 
					            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='تاریخ حذف')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='نام شرکت سازنده کنتور آب')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'verbose_name': 'شرکت سازنده کنتور آب',
 | 
				
			||||||
 | 
					                'verbose_name_plural': 'شرکت\u200cهای سازنده کنتور آب',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        migrations.CreateModel(
 | 
					        migrations.CreateModel(
 | 
				
			||||||
            name='HistoricalWell',
 | 
					            name='HistoricalWell',
 | 
				
			||||||
            fields=[
 | 
					            fields=[
 | 
				
			||||||
| 
						 | 
					@ -25,12 +43,28 @@ class Migration(migrations.Migration):
 | 
				
			||||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
					                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
				
			||||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
					                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
				
			||||||
                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
					                ('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
 | 
				
			||||||
 | 
					                ('water_subscription_number', models.CharField(db_index=True, max_length=20, verbose_name='شماره اشتراک آب')),
 | 
				
			||||||
 | 
					                ('electricity_subscription_number', models.CharField(db_index=True, max_length=20, null=True, verbose_name='شماره اشتراک برق')),
 | 
				
			||||||
 | 
					                ('water_meter_serial_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور آب')),
 | 
				
			||||||
 | 
					                ('water_meter_old_serial_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور قدیمی آب')),
 | 
				
			||||||
 | 
					                ('utm_x', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='X UTM')),
 | 
				
			||||||
 | 
					                ('utm_y', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='Y UTM')),
 | 
				
			||||||
 | 
					                ('utm_zone', models.PositiveIntegerField(blank=True, default=40, null=True, verbose_name='زون UTM')),
 | 
				
			||||||
 | 
					                ('utm_hemisphere', models.CharField(blank=True, choices=[('N', 'شمال'), ('S', 'جنوب')], default='N', max_length=1, null=True, verbose_name='نیمکره UTM')),
 | 
				
			||||||
 | 
					                ('well_power', models.PositiveIntegerField(blank=True, null=True, verbose_name='قدرت چاه')),
 | 
				
			||||||
 | 
					                ('reference_letter_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره معرفی نامه')),
 | 
				
			||||||
 | 
					                ('reference_letter_date', models.DateField(blank=True, null=True, verbose_name='تاریخ معرفی نامه')),
 | 
				
			||||||
 | 
					                ('representative_letter_file', models.TextField(blank=True, max_length=100, null=True, verbose_name='نامه نمایندگی')),
 | 
				
			||||||
                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
					                ('history_id', models.AutoField(primary_key=True, serialize=False)),
 | 
				
			||||||
                ('history_date', models.DateTimeField(db_index=True)),
 | 
					                ('history_date', models.DateTimeField(db_index=True)),
 | 
				
			||||||
                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
					                ('history_change_reason', models.CharField(max_length=100, null=True)),
 | 
				
			||||||
                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
					                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
 | 
				
			||||||
 | 
					                ('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
 | 
				
			||||||
 | 
					                ('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
 | 
				
			||||||
 | 
					                ('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
 | 
				
			||||||
                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
					                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
				
			||||||
                ('representative', 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='نماینده')),
 | 
					                ('representative', 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='نماینده')),
 | 
				
			||||||
 | 
					                ('water_meter_manufacturer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب')),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                'verbose_name': 'historical چاه',
 | 
					                'verbose_name': 'historical چاه',
 | 
				
			||||||
| 
						 | 
					@ -50,7 +84,23 @@ class Migration(migrations.Migration):
 | 
				
			||||||
                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
					                ('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
 | 
				
			||||||
                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
					                ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
 | 
				
			||||||
                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
					                ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
 | 
				
			||||||
                ('representative', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wells', to=settings.AUTH_USER_MODEL, verbose_name='نماینده')),
 | 
					                ('water_subscription_number', models.CharField(max_length=20, unique=True, verbose_name='شماره اشتراک آب')),
 | 
				
			||||||
 | 
					                ('electricity_subscription_number', models.CharField(max_length=20, null=True, unique=True, verbose_name='شماره اشتراک برق')),
 | 
				
			||||||
 | 
					                ('water_meter_serial_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور آب')),
 | 
				
			||||||
 | 
					                ('water_meter_old_serial_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور قدیمی آب')),
 | 
				
			||||||
 | 
					                ('utm_x', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='X UTM')),
 | 
				
			||||||
 | 
					                ('utm_y', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='Y UTM')),
 | 
				
			||||||
 | 
					                ('utm_zone', models.PositiveIntegerField(blank=True, default=40, null=True, verbose_name='زون UTM')),
 | 
				
			||||||
 | 
					                ('utm_hemisphere', models.CharField(blank=True, choices=[('N', 'شمال'), ('S', 'جنوب')], default='N', max_length=1, null=True, verbose_name='نیمکره UTM')),
 | 
				
			||||||
 | 
					                ('well_power', models.PositiveIntegerField(blank=True, null=True, verbose_name='قدرت چاه')),
 | 
				
			||||||
 | 
					                ('reference_letter_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره معرفی نامه')),
 | 
				
			||||||
 | 
					                ('reference_letter_date', models.DateField(blank=True, null=True, verbose_name='تاریخ معرفی نامه')),
 | 
				
			||||||
 | 
					                ('representative_letter_file', models.FileField(blank=True, null=True, upload_to=wells.models.Well.path_and_rename, verbose_name='نامه نمایندگی')),
 | 
				
			||||||
 | 
					                ('affairs', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور')),
 | 
				
			||||||
 | 
					                ('broker', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار')),
 | 
				
			||||||
 | 
					                ('county', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان')),
 | 
				
			||||||
 | 
					                ('representative', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells', to=settings.AUTH_USER_MODEL, verbose_name='نماینده')),
 | 
				
			||||||
 | 
					                ('water_meter_manufacturer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب')),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                'verbose_name': 'چاه',
 | 
					                'verbose_name': 'چاه',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,202 +0,0 @@
 | 
				
			||||||
# Generated by Django 5.2.4 on 2025-08-07 14:29
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import datetime
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
import django.utils.timezone
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('locations', '0001_initial'),
 | 
					 | 
				
			||||||
        ('wells', '0001_initial'),
 | 
					 | 
				
			||||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='WaterMeterManufacturer',
 | 
					 | 
				
			||||||
            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='تاریخ حذف')),
 | 
					 | 
				
			||||||
                ('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='نام شرکت سازنده کنتور آب')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            options={
 | 
					 | 
				
			||||||
                'verbose_name': 'شرکت سازنده کنتور آب',
 | 
					 | 
				
			||||||
                'verbose_name_plural': 'شرکت\u200cهای سازنده کنتور آب',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='affairs',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='broker',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='county',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='electricity_subscription_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, db_index=True, max_length=20, null=True, verbose_name='شماره اشتراک برق'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='reference_letter_date',
 | 
					 | 
				
			||||||
            field=models.DateField(blank=True, null=True, verbose_name='تاریخ معرفی نامه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='reference_letter_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره معرفی نامه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='representative_letter_file',
 | 
					 | 
				
			||||||
            field=models.TextField(blank=True, max_length=100, null=True, verbose_name='نامه نمایندگی'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='utm_hemisphere',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, choices=[('N', 'شمال'), ('S', 'جنوب')], default='N', max_length=1, null=True, verbose_name='نیمکره UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='utm_x',
 | 
					 | 
				
			||||||
            field=models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='X UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='utm_y',
 | 
					 | 
				
			||||||
            field=models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='Y UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='utm_zone',
 | 
					 | 
				
			||||||
            field=models.PositiveIntegerField(blank=True, default=40, null=True, verbose_name='زون UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='water_meter_old_serial_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور قدیمی آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='water_meter_serial_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='water_subscription_number',
 | 
					 | 
				
			||||||
            field=models.CharField(db_index=True, default=datetime.datetime(2025, 8, 7, 14, 29, 15, 340093, tzinfo=datetime.timezone.utc), max_length=20, verbose_name='شماره اشتراک آب'),
 | 
					 | 
				
			||||||
            preserve_default=False,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='well_power',
 | 
					 | 
				
			||||||
            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قدرت چاه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='affairs',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='broker',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='county',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='electricity_subscription_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, unique=True, verbose_name='شماره اشتراک برق'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='reference_letter_date',
 | 
					 | 
				
			||||||
            field=models.DateField(blank=True, null=True, verbose_name='تاریخ معرفی نامه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='reference_letter_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره معرفی نامه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='representative_letter_file',
 | 
					 | 
				
			||||||
            field=models.FileField(blank=True, null=True, upload_to='representative_letters/', verbose_name='نامه نمایندگی'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='utm_hemisphere',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, choices=[('N', 'شمال'), ('S', 'جنوب')], default='N', max_length=1, null=True, verbose_name='نیمکره UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='utm_x',
 | 
					 | 
				
			||||||
            field=models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='X UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='utm_y',
 | 
					 | 
				
			||||||
            field=models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='Y UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='utm_zone',
 | 
					 | 
				
			||||||
            field=models.PositiveIntegerField(blank=True, default=40, null=True, verbose_name='زون UTM'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='water_meter_old_serial_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور قدیمی آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='water_meter_serial_number',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='سریال کنتور آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='water_subscription_number',
 | 
					 | 
				
			||||||
            field=models.CharField(default=django.utils.timezone.now, max_length=20, unique=True, verbose_name='شماره اشتراک آب'),
 | 
					 | 
				
			||||||
            preserve_default=False,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='well_power',
 | 
					 | 
				
			||||||
            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قدرت چاه'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='representative',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells', to=settings.AUTH_USER_MODEL, verbose_name='نماینده'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='historicalwell',
 | 
					 | 
				
			||||||
            name='water_meter_manufacturer',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='well',
 | 
					 | 
				
			||||||
            name='water_meter_manufacturer',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,6 @@ class Well(SluggedModel):
 | 
				
			||||||
        verbose_name="نماینده",
 | 
					        verbose_name="نماینده",
 | 
				
			||||||
        related_name="wells",
 | 
					        related_name="wells",
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        blank=True
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    water_subscription_number = models.CharField(
 | 
					    water_subscription_number = models.CharField(
 | 
				
			||||||
| 
						 | 
					@ -54,7 +53,6 @@ class Well(SluggedModel):
 | 
				
			||||||
        max_length=20,
 | 
					        max_length=20,
 | 
				
			||||||
        verbose_name="شماره اشتراک برق",
 | 
					        verbose_name="شماره اشتراک برق",
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        blank=True,
 | 
					 | 
				
			||||||
        unique=True
 | 
					        unique=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -387,7 +387,8 @@
 | 
				
			||||||
          observer: true,
 | 
					          observer: true,
 | 
				
			||||||
          calendar: {
 | 
					          calendar: {
 | 
				
			||||||
            persian: {
 | 
					            persian: {
 | 
				
			||||||
              locale: 'fa'
 | 
					              locale: 'fa',
 | 
				
			||||||
 | 
					              leapYearMode: 'astronomical'
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onSelect: function(unix) {
 | 
					          onSelect: function(unix) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue