Merge remote-tracking branch 'origin/main' into shafafiyat/production
This commit is contained in:
		
						commit
						20f00b786e
					
				
					 54 changed files with 2391 additions and 758 deletions
				
			
		| 
						 | 
				
			
			@ -33,9 +33,9 @@ class ProfileAdmin(admin.ModelAdmin):
 | 
			
		|||
 | 
			
		||||
@admin.register(Company)
 | 
			
		||||
class CompanyAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'logo', 'signature', 'address', 'phone']
 | 
			
		||||
    list_display = ['name', 'logo', 'signature', 'address', 'phone', 'broker', 'registration_number']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
    search_fields = ['name', 'address', 'phone']
 | 
			
		||||
    list_filter = ['is_active']
 | 
			
		||||
    list_filter = ['is_active', 'broker']
 | 
			
		||||
    date_hierarchy = 'created'
 | 
			
		||||
    ordering = ['-created']
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +90,19 @@ class CustomerForm(forms.ModelForm):
 | 
			
		|||
        return national_code
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        def _compute_completed(cleaned):
 | 
			
		||||
            try:
 | 
			
		||||
                first_ok = bool((cleaned.get('first_name') or '').strip())
 | 
			
		||||
                last_ok = bool((cleaned.get('last_name') or '').strip())
 | 
			
		||||
                nc_ok = bool((cleaned.get('national_code') or '').strip())
 | 
			
		||||
                phone_ok = bool((cleaned.get('phone_number_1') or '').strip() or (cleaned.get('phone_number_2') or '').strip())
 | 
			
		||||
                addr_ok = bool((cleaned.get('address') or '').strip())
 | 
			
		||||
                bank_ok = bool(cleaned.get('bank_name'))
 | 
			
		||||
                card_ok = bool((cleaned.get('card_number') or '').strip())
 | 
			
		||||
                acc_ok = bool((cleaned.get('account_number') or '').strip())
 | 
			
		||||
                return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok])
 | 
			
		||||
            except Exception:
 | 
			
		||||
                return False
 | 
			
		||||
        # Check if this is an update (instance exists)
 | 
			
		||||
        if self.instance and self.instance.pk:
 | 
			
		||||
            # Update existing profile
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +121,18 @@ class CustomerForm(forms.ModelForm):
 | 
			
		|||
                    profile.affairs = current_user_profile.affairs
 | 
			
		||||
                    profile.county = current_user_profile.county
 | 
			
		||||
                    profile.broker = current_user_profile.broker
 | 
			
		||||
            # Set completion flag based on provided form data
 | 
			
		||||
            profile.is_completed = _compute_completed({
 | 
			
		||||
                'first_name': user.first_name,
 | 
			
		||||
                'last_name': user.last_name,
 | 
			
		||||
                'national_code': self.cleaned_data.get('national_code'),
 | 
			
		||||
                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
			
		||||
                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
			
		||||
                'address': self.cleaned_data.get('address'),
 | 
			
		||||
                'bank_name': self.cleaned_data.get('bank_name'),
 | 
			
		||||
                'card_number': self.cleaned_data.get('card_number'),
 | 
			
		||||
                'account_number': self.cleaned_data.get('account_number'),
 | 
			
		||||
            })
 | 
			
		||||
            
 | 
			
		||||
            if commit:
 | 
			
		||||
                profile.save()
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +167,18 @@ class CustomerForm(forms.ModelForm):
 | 
			
		|||
                    profile.affairs = current_user_profile.affairs
 | 
			
		||||
                    profile.county = current_user_profile.county
 | 
			
		||||
                    profile.broker = current_user_profile.broker
 | 
			
		||||
            # Set completion flag based on provided form data
 | 
			
		||||
            profile.is_completed = _compute_completed({
 | 
			
		||||
                'first_name': user.first_name,
 | 
			
		||||
                'last_name': user.last_name,
 | 
			
		||||
                'national_code': self.cleaned_data.get('national_code'),
 | 
			
		||||
                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
			
		||||
                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
			
		||||
                'address': self.cleaned_data.get('address'),
 | 
			
		||||
                'bank_name': self.cleaned_data.get('bank_name'),
 | 
			
		||||
                'card_number': self.cleaned_data.get('card_number'),
 | 
			
		||||
                'account_number': self.cleaned_data.get('account_number'),
 | 
			
		||||
            })
 | 
			
		||||
            
 | 
			
		||||
            if commit:
 | 
			
		||||
                profile.save()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								accounts/migrations/0002_company_broker.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								accounts/migrations/0002_company_broker.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 13:43
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0001_initial'),
 | 
			
		||||
        ('locations', '0003_remove_broker_company'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='broker',
 | 
			
		||||
            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='company', to='locations.broker', verbose_name='کارگزار'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 14:11
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0002_company_broker'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='account_number',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='bank_name',
 | 
			
		||||
            field=models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='card_number',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='sheba_number',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=30, null=True, verbose_name='شماره شبا'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								accounts/migrations/0004_company_branch_name.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								accounts/migrations/0004_company_branch_name.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 14:12
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0003_company_account_number_company_bank_name_and_more'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='branch_name',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شعبه بانک'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								accounts/migrations/0005_company_registration_number.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								accounts/migrations/0005_company_registration_number.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-08 10:10
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0004_company_branch_name'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='registration_number',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شماره ثبت شرکت'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								accounts/migrations/0006_company_card_holder_name.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								accounts/migrations/0006_company_card_holder_name.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-08 10:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0005_company_registration_number'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='company',
 | 
			
		||||
            name='card_holder_name',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='نام دارنده کارت'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -181,11 +181,94 @@ class Profile(BaseModel):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class Company(NameSlugModel):
 | 
			
		||||
    logo = models.ImageField(upload_to='companies/logos', null=True, blank=True, verbose_name='لوگوی شرکت')
 | 
			
		||||
    signature = models.ImageField(upload_to='companies/signatures', null=True, blank=True, verbose_name='امضای شرکت')
 | 
			
		||||
    address = models.TextField(null=True, blank=True, verbose_name='آدرس')
 | 
			
		||||
    phone = models.CharField(max_length=11, null=True, blank=True, verbose_name='شماره تماس')
 | 
			
		||||
   
 | 
			
		||||
    logo = models.ImageField(
 | 
			
		||||
        upload_to='companies/logos',
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name='لوگوی شرکت'
 | 
			
		||||
        )
 | 
			
		||||
    signature = models.ImageField(
 | 
			
		||||
        upload_to='companies/signatures',
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name='امضای شرکت'
 | 
			
		||||
        )
 | 
			
		||||
    address = models.TextField(
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name='آدرس'
 | 
			
		||||
        )
 | 
			
		||||
    phone = models.CharField(
 | 
			
		||||
        max_length=11,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name='شماره تماس'
 | 
			
		||||
        )
 | 
			
		||||
    registration_number = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name='شماره ثبت شرکت'
 | 
			
		||||
        )
 | 
			
		||||
    broker = models.OneToOneField(
 | 
			
		||||
        Broker,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="کارگزار",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name='company'
 | 
			
		||||
        )
 | 
			
		||||
    card_number = models.CharField(
 | 
			
		||||
        max_length=16,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره کارت",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r'^\d+$',
 | 
			
		||||
                message='شماره کارت باید فقط شامل اعداد باشد.',
 | 
			
		||||
                code='invalid_card_number'
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    account_number = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره حساب",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        validators=[
 | 
			
		||||
            RegexValidator(
 | 
			
		||||
                regex=r'^\d+$',
 | 
			
		||||
                message='شماره حساب باید فقط شامل اعداد باشد.',
 | 
			
		||||
                code='invalid_account_number'
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    card_holder_name = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="نام دارنده کارت",
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    sheba_number = models.CharField(
 | 
			
		||||
        max_length=30,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شماره شبا",
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    bank_name = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        choices=BANK_CHOICES,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="نام بانک",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    branch_name = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        null=True,
 | 
			
		||||
        verbose_name="شعبه بانک",
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = 'شرکت'
 | 
			
		||||
        verbose_name_plural = 'شرکتها'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -172,12 +172,19 @@
 | 
			
		|||
          </tr>
 | 
			
		||||
                      {% empty %}
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td colspan="7" class="text-center py-4">
 | 
			
		||||
              <td class="text-center py-4">
 | 
			
		||||
                <div class="d-flex flex-column align-items-center">
 | 
			
		||||
                  <i class="bx bx-user-x bx-lg text-muted mb-2"></i>
 | 
			
		||||
                  <span class="text-muted">هیچ کاربری یافت نشد</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
              <td></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,9 @@ layout-wide customizer-hide
 | 
			
		|||
{% endblock style %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl">
 | 
			
		||||
  <div class="authentication-wrapper authentication-basic container-p-y">
 | 
			
		||||
    <div class="authentication-inner">
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +72,7 @@ layout-wide customizer-hide
 | 
			
		|||
            {% csrf_token %}
 | 
			
		||||
            <div class="mb-3 fv-plugins-icon-container">
 | 
			
		||||
              <label for="email" class="form-label">نام کاربری</label>
 | 
			
		||||
              <input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
 | 
			
		||||
              <input type="text" class="form-control" id="email" name="username" placeholder="نام کاربری خود را وارد کنید" autofocus="">
 | 
			
		||||
            <div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
 | 
			
		||||
            <div class="mb-3 form-password-toggle fv-plugins-icon-container">
 | 
			
		||||
              <div class="d-flex justify-content-between">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ from accounts.views import login_view, dashboard, customer_list, add_customer_aj
 | 
			
		|||
 | 
			
		||||
app_name = "accounts"
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login/', login_view, name='login'),
 | 
			
		||||
    path('', login_view, name='login'),
 | 
			
		||||
    path('logout/', logout_view, name='logout'),
 | 
			
		||||
    path('dashboard/', dashboard, name='dashboard'),
 | 
			
		||||
    path('customers/', customer_list, name='customer_list'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,9 @@ from django import forms
 | 
			
		|||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from accounts.models import Profile
 | 
			
		||||
from accounts.forms import CustomerForm
 | 
			
		||||
from processes.utils import scope_customers_queryset
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from common.decorators import allowed_roles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +19,9 @@ def login_view(request):
 | 
			
		|||
    renders login page and authenticating user POST requests
 | 
			
		||||
    to log user in
 | 
			
		||||
    """
 | 
			
		||||
    # If already authenticated, go straight to request list
 | 
			
		||||
    if request.user.is_authenticated:
 | 
			
		||||
        return redirect("processes:request_list")
 | 
			
		||||
    if request.method == "POST":
 | 
			
		||||
        username = request.POST.get("username")
 | 
			
		||||
        password = request.POST.get("password")
 | 
			
		||||
| 
						 | 
				
			
			@ -35,9 +40,11 @@ def dashboard(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def customer_list(request):
 | 
			
		||||
    # Get all profiles that have customer role
 | 
			
		||||
    customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
 | 
			
		||||
    base = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
 | 
			
		||||
    customers = scope_customers_queryset(request.user, base)
 | 
			
		||||
    
 | 
			
		||||
    form = CustomerForm()
 | 
			
		||||
    return render(request, "accounts/customer_list.html", {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +54,8 @@ def customer_list(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def add_customer_ajax(request):
 | 
			
		||||
    """AJAX endpoint for adding customers"""
 | 
			
		||||
    form = CustomerForm(request.POST, request.FILES)
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +94,8 @@ def add_customer_ajax(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def edit_customer_ajax(request, customer_id):
 | 
			
		||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
			
		||||
    form = CustomerForm(request.POST, request.FILES, instance=customer)
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +133,7 @@ def edit_customer_ajax(request, customer_id):
 | 
			
		|||
        })
 | 
			
		||||
 | 
			
		||||
@require_GET
 | 
			
		||||
@login_required
 | 
			
		||||
def get_customer_data(request, customer_id):
 | 
			
		||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -162,6 +174,7 @@ def get_customer_data(request, customer_id):
 | 
			
		|||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def logout_view(request):
 | 
			
		||||
    """Log out current user and redirect to login page."""
 | 
			
		||||
    logout(request)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from common.models import BaseModel
 | 
			
		||||
from _helpers.utils import jalali_converter2
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -35,4 +36,7 @@ class CertificateInstance(BaseModel):
 | 
			
		|||
    def __str__(self):
 | 
			
		||||
        return f"گواهی {self.process_instance.code}"
 | 
			
		||||
 | 
			
		||||
    def jissued_at(self):
 | 
			
		||||
        return jalali_converter2(self.issued_at)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,28 +1,71 @@
 | 
			
		|||
{% extends '_base.html' %}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="fa" dir="rtl">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>تاییدیه - {{ instance.code }}</title>
 | 
			
		||||
  {% load static %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container py-4">
 | 
			
		||||
  <div class="text-center mb-4">
 | 
			
		||||
  <!-- Fonts (match project) -->
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
			
		||||
  <link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
 | 
			
		||||
 | 
			
		||||
  <!-- Core CSS (same as other prints) -->
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
			
		||||
 | 
			
		||||
  <style>
 | 
			
		||||
    @page { size: A4; margin: 1cm; }
 | 
			
		||||
    @media print { body { print-color-adjust: exact; } .no-print { display: none !important; } }
 | 
			
		||||
    .header { border-bottom: 1px solid #dee2e6; padding-bottom: 16px; margin-bottom: 24px; }
 | 
			
		||||
    .company-name { font-weight: 600; }
 | 
			
		||||
    .body-text { white-space: pre-line; line-height: 1.9; }
 | 
			
		||||
    .signature-section { margin-top: 40px; border-top: 1px solid #dee2e6; padding-top: 24px; }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div class="container-fluid py-3">
 | 
			
		||||
    <!-- Top-left request info -->
 | 
			
		||||
    <div class="d-flex mb-2">
 | 
			
		||||
      <div class="ms-auto text-end">
 | 
			
		||||
        <div class="">شماره درخواست: {{ instance.code }}</div>
 | 
			
		||||
        <div class="">تاریخ: {{ cert.jissued_at }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Header with logo and company -->
 | 
			
		||||
    <div class="header text-center">
 | 
			
		||||
      {% if template.company and template.company.logo %}
 | 
			
		||||
        <img src="{{ template.company.logo.url }}" alt="logo" style="max-height:90px">
 | 
			
		||||
      {% endif %}
 | 
			
		||||
      <h4 class="mt-2">{{ cert.rendered_title }}</h4>
 | 
			
		||||
    {% if template.company %}<div class="text-muted">{{ template.company.name }}</div>{% endif %}
 | 
			
		||||
      {% if template.company %}
 | 
			
		||||
        <div class="text-muted company-name">{{ template.company.name }}</div>
 | 
			
		||||
      {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
  <div style="white-space:pre-line; line-height:1.9;">
 | 
			
		||||
 | 
			
		||||
    <!-- Certificate body -->
 | 
			
		||||
    <div class="body-text">
 | 
			
		||||
      {{ cert.rendered_body|safe }}
 | 
			
		||||
    </div>
 | 
			
		||||
  <div class="mt-5 d-flex justify-content-between">
 | 
			
		||||
    <div>تاریخ: {{ cert.issued_at }}</div>
 | 
			
		||||
 | 
			
		||||
    <!-- Signature -->
 | 
			
		||||
    <div class="signature-section d-flex justify-content-end">
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <div>مهر و امضای تایید کننده</div>
 | 
			
		||||
        <div class="text-muted">{{ template.company.name }}</div>
 | 
			
		||||
        {% if template.company and template.company.signature %}
 | 
			
		||||
        <img src="{{ template.company.signature.url }}" alt="seal" style="max-height:120px">
 | 
			
		||||
          <img src="{{ template.company.signature.url }}" alt="seal" style="max-height:200px">
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      <div>مهر و امضای شرکت</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<script>window.print()</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
    window.onload = function() { window.print(); setTimeout(function(){ window.close(); }, 200); };
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,40 +18,49 @@
 | 
			
		|||
  <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' %}
 | 
			
		||||
 | 
			
		||||
  <!-- Instance Info Modal -->
 | 
			
		||||
  {% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
  {% 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 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:"-" }}
 | 
			
		||||
              {% instance_info instance %}
 | 
			
		||||
            </small>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="d-flex gap-2">
 | 
			
		||||
            <a class="btn btn-outline-secondary" target="_blank" href="{% url 'certificates:certificate_print' instance.id %}"><i class="bx bx-printer"></i> پرینت</a>
 | 
			
		||||
            <a class="btn btn-outline-secondary" target="_blank" href="{% url 'certificates:certificate_print' instance.id %}">
 | 
			
		||||
              <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
            <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
              <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
              بازگشت
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
  
 | 
			
		||||
        <div class="bs-stepper wizard-vertical vertical mt-2 no-print">
 | 
			
		||||
        <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
			
		||||
          {% stepper_header instance step %}
 | 
			
		||||
          <div class="bs-stepper-content">
 | 
			
		||||
 | 
			
		||||
            <div class="card">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <div class="d-flex mb-2">
 | 
			
		||||
                  <div class="ms-auto text-end">
 | 
			
		||||
                    <div>شماره درخواست: {{ instance.code }}</div>
 | 
			
		||||
                    <div>تاریخ: {{ cert.jissued_at }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-center mb-3">
 | 
			
		||||
                  {% if template.company and template.company.logo %}
 | 
			
		||||
                    <img src="{{ template.company.logo.url }}" alt="logo" style="max-height:80px">
 | 
			
		||||
| 
						 | 
				
			
			@ -62,21 +71,22 @@
 | 
			
		|||
                <div class="mt-3" style="white-space:pre-line; line-height:1.9;">
 | 
			
		||||
                  {{ cert.rendered_body|safe }}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="mt-4 d-flex justify-content-between align-items-end">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <div>تاریخ صدور: {{ cert.issued_at }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                <div class="signature-section d-flex justify-content-end">
 | 
			
		||||
                  <div class="text-center">
 | 
			
		||||
                    <div>مهر و امضای تایید کننده</div>
 | 
			
		||||
                    <div class="text-muted">{{ template.company.name }}</div>
 | 
			
		||||
                    {% if template.company and template.company.signature %}
 | 
			
		||||
                      <img src="{{ template.company.signature.url }}" alt="seal" style="max-height:100px">
 | 
			
		||||
                      <img src="{{ template.company.signature.url }}" alt="seal" style="max-height:200px">
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <div>مهر و امضای شرکت</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="card-footer d-flex justify-content-between">
 | 
			
		||||
                {% if previous_step %}
 | 
			
		||||
                  <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
 | 
			
		||||
                  <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>
 | 
			
		||||
                    قبلی
 | 
			
		||||
                  </a>
 | 
			
		||||
                {% else %}<span></span>{% endif %}
 | 
			
		||||
                <form method="post">
 | 
			
		||||
                  {% csrf_token %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ from .models import CertificateTemplate, CertificateInstance
 | 
			
		|||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
from _helpers.jalali import Gregorian
 | 
			
		||||
from processes.utils import get_scoped_instance_or_404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _to_jalali(date_obj):
 | 
			
		||||
| 
						 | 
				
			
			@ -33,23 +34,25 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance):
 | 
			
		|||
        'company_name': (template.company.name if template.company else '') or '',
 | 
			
		||||
        'customer_full_name': rep.get_full_name() if rep else '',
 | 
			
		||||
        'water_subscription_number': getattr(well, 'water_subscription_number', '') or '',
 | 
			
		||||
        'address': getattr(well, 'address', '') or '',
 | 
			
		||||
        'address': getattr(well, 'county', '') or '',
 | 
			
		||||
        'visit_date_jalali': _to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else '',
 | 
			
		||||
    }
 | 
			
		||||
    title = (template.title or '').format(**ctx)
 | 
			
		||||
    body = (template.body or '')
 | 
			
		||||
    # Render body placeholders with bold values
 | 
			
		||||
    for k, v in ctx.items():
 | 
			
		||||
        body = body.replace(f"{{{{ {k} }}}}", str(v))
 | 
			
		||||
        body = body.replace(f"{{{{ {k} }}}}", f"<strong>{str(v)}</strong>")
 | 
			
		||||
    return title, body
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def certificate_step(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    # Ensure all previous steps are completed and invoice settled
 | 
			
		||||
    prior_steps = instance.process.steps.filter(order__lt=instance.current_step.order if instance.current_step else 9999)
 | 
			
		||||
    incomplete = StepInstance.objects.filter(process_instance=instance, step__in=prior_steps).exclude(status='completed').exists()
 | 
			
		||||
 | 
			
		||||
    if incomplete:
 | 
			
		||||
        messages.error(request, 'ابتدا همه مراحل قبلی را تکمیل کنید')
 | 
			
		||||
        return redirect('processes:request_list')
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +90,17 @@ def certificate_step(request, instance_id, step_id):
 | 
			
		|||
        except Exception:
 | 
			
		||||
            messages.error(request, 'شما مجوز تایید این مرحله را ندارید')
 | 
			
		||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
        # Safety check: ensure ALL previous steps are completed before approval
 | 
			
		||||
        try:
 | 
			
		||||
            prev_steps_qs = instance.process.steps.filter(order__lt=step.order)
 | 
			
		||||
            has_incomplete = StepInstance.objects.filter(process_instance=instance, step__in=prev_steps_qs).exclude(status='completed').exists()
 | 
			
		||||
            if has_incomplete:
 | 
			
		||||
                messages.error(request, 'ابتدا همه مراحل قبلی را تکمیل کنید')
 | 
			
		||||
                return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            messages.error(request, 'خطا در بررسی مراحل قبلی')
 | 
			
		||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
            
 | 
			
		||||
        cert.approved = True
 | 
			
		||||
        cert.approved_at = timezone.now()
 | 
			
		||||
        cert.save()
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +129,7 @@ def certificate_step(request, instance_id, step_id):
 | 
			
		|||
 | 
			
		||||
@login_required
 | 
			
		||||
def certificate_print(request, instance_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
 | 
			
		||||
    template = cert.template if cert else None
 | 
			
		||||
    return render(request, 'certificates/print.html', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ from functools import wraps
 | 
			
		|||
from django.http import JsonResponse, HttpResponse
 | 
			
		||||
from django.shortcuts import redirect
 | 
			
		||||
 | 
			
		||||
from extensions.consts import UserRoles
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_ajax(view_func):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,45 +2,71 @@
 | 
			
		|||
<html lang="fa" dir="rtl">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>چاپ قرارداد {{ instance.code }}</title>
 | 
			
		||||
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
			
		||||
  {% load static %}
 | 
			
		||||
 | 
			
		||||
  <!-- Match app fonts and theme -->
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
			
		||||
  <link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500,1,600,1,700&display=swap" rel="stylesheet">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
			
		||||
 | 
			
		||||
  <style>
 | 
			
		||||
    @page { size: A4; margin: 1.2cm; }
 | 
			
		||||
    body { font-family: 'Vazirmatn', sans-serif; }
 | 
			
		||||
    .logo { max-height: 80px; }
 | 
			
		||||
    .signature { height: 90px; border: 1px dashed #ccc; }
 | 
			
		||||
    .invoice-header { border-bottom: 1px solid #dee2e6; padding-bottom: 16px; margin-bottom: 24px; }
 | 
			
		||||
    .brand-box { width:64px; height:64px; display:flex; align-items:center; justify-content:center; background:#eef2ff; border-radius:8px; }
 | 
			
		||||
    .logo { max-height: 58px; max-width: 120px; }
 | 
			
		||||
    .contract-title { font-size: 20px; font-weight: 600; }
 | 
			
		||||
    .small-muted { font-size: 12px; color: #6c757d; }
 | 
			
		||||
    .signature-box { border: 1px dashed #ccc; height: 210px; display:flex; align-items:center; justify-content:center; }
 | 
			
		||||
  </style>
 | 
			
		||||
  <script>
 | 
			
		||||
    window.addEventListener('load', function(){ setTimeout(function(){ window.print(); }, 300); });
 | 
			
		||||
    window.onload = function(){
 | 
			
		||||
      window.print();
 | 
			
		||||
      setTimeout(function(){ window.close(); }, 200);
 | 
			
		||||
    };
 | 
			
		||||
  </script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div class="container-fluid">
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
      <div>
 | 
			
		||||
        <h5>{{ contract.template.company.name }}</h5>
 | 
			
		||||
        <h5 class="mb-1">{{ contract.template.name }}</h5>
 | 
			
		||||
        <div class="text-muted small">کد درخواست: {{ instance.code }} | تاریخ: {{ contract.jcreated }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {% if contract.template.company.logo %}
 | 
			
		||||
        <img class="logo" src="{{ contract.template.company.logo.url }}" alt="لوگو" />
 | 
			
		||||
    <!-- Header: Company and contract info -->
 | 
			
		||||
    <div class="invoice-header">
 | 
			
		||||
      <div class="small text-end text-muted mb-2">تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}</div>
 | 
			
		||||
      <h5 class="text-center mb-3">
 | 
			
		||||
        {% if instance.broker and instance.broker.company %}
 | 
			
		||||
          {{ instance.broker.company.name }}
 | 
			
		||||
        {% elif template.company %}
 | 
			
		||||
          {{ template.company.name }}
 | 
			
		||||
        {% else %}
 | 
			
		||||
          شرکت آب منطقهای
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      
 | 
			
		||||
      </h5>
 | 
			
		||||
      <h4 class="text-center mb-3">{{ contract.template.name }}</h4>
 | 
			
		||||
    </div>
 | 
			
		||||
    <hr>
 | 
			
		||||
 | 
			
		||||
    <!-- Contract body -->
 | 
			
		||||
    <div style="white-space: pre-line; line-height: 1.9;">{{ contract.rendered_body|safe }}</div>
 | 
			
		||||
    <hr>
 | 
			
		||||
 | 
			
		||||
    <!-- Signatures -->
 | 
			
		||||
    <div class="row mt-4">
 | 
			
		||||
      <div class="col-6 text-center">
 | 
			
		||||
        <div>امضای مشترک</div>
 | 
			
		||||
        <div class="signature mt-2"></div>
 | 
			
		||||
        <div class="signature-box mt-2"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-6 text-center">
 | 
			
		||||
        <div>امضای شرکت</div>
 | 
			
		||||
        <div class="signature mt-2">
 | 
			
		||||
          {% if contract.template.company.signature %}
 | 
			
		||||
            <img src="{{ contract.template.company.signature.url }}" alt="امضای شرکت" style="max-height: 80px;" />
 | 
			
		||||
        <div class="signature-box mt-2">
 | 
			
		||||
          {% if instance.broker and instance.broker.company and instance.broker.company.signature %}
 | 
			
		||||
            <img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="max-height: 200px;" />
 | 
			
		||||
          {% elif contract.template.company and contract.template.company.signature %}
 | 
			
		||||
            <img src="{{ contract.template.company.signature.url }}" alt="امضای شرکت" style="max-height: 200px;" />
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,10 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-12 mb-4">
 | 
			
		||||
| 
						 | 
				
			
			@ -26,13 +30,18 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="d-flex gap-2">
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
          <a href="{% url 'contracts:contract_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">پرینت</a>
 | 
			
		||||
          <a href="{% url 'contracts:contract_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
          </a>
 | 
			
		||||
          
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,29 +50,32 @@
 | 
			
		|||
        <div class="bs-stepper-content">
 | 
			
		||||
          <div class="card border">
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
              <div class="small text-end text-muted mb-2">تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}</div>
 | 
			
		||||
              <h5 class="text-center mb-3">
 | 
			
		||||
                {% if instance.broker and instance.broker.company %}
 | 
			
		||||
                {{ instance.broker.company.name }}
 | 
			
		||||
              {% elif template.company %}
 | 
			
		||||
                {{ template.company.name }}
 | 
			
		||||
              {% else %}
 | 
			
		||||
                شرکت آب منطقهای
 | 
			
		||||
              {% endif %}</h5>
 | 
			
		||||
              <h4 class="text-center mb-3">{{ contract.template.name }}</h4>
 | 
			
		||||
              {% if can_view_contract_body %}
 | 
			
		||||
                {% if template.company.logo %}
 | 
			
		||||
                  <div class="text-center mb-3">
 | 
			
		||||
                    <img src="{{ template.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
 | 
			
		||||
                    <h4 class="text-muted">{{ contract.template.company.name }}</h4>
 | 
			
		||||
                    <h5 class="text-muted">{{ contract.template.name }}</h5>
 | 
			
		||||
                  </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                <div class="small text-muted mb-2">تاریخ: {{ contract.jcreated }}</div>
 | 
			
		||||
                <hr>
 | 
			
		||||
                <div class="contract-body" style="white-space: pre-line; line-height:1.9;">{{ contract.rendered_body|safe }}</div>
 | 
			
		||||
                <hr>
 | 
			
		||||
                <div class="row mt-4">
 | 
			
		||||
                  <div class="col-6 text-center">
 | 
			
		||||
                    <div>امضای مشترک</div>
 | 
			
		||||
                    <div style="height:90px;border:1px dashed #ccc; margin-top:10px;"></div>
 | 
			
		||||
                    <div style="height:210px;border:1px dashed #ccc; margin-top:10px;"></div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-6 text-center">
 | 
			
		||||
                    <div>امضای شرکت</div>
 | 
			
		||||
                    <div style="height:90px;border:1px dashed #ccc; margin-top:10px;">
 | 
			
		||||
                      {% if template.company.signature %}
 | 
			
		||||
                        <img src="{{ template.company.signature.url }}" alt="امضای شرکت" style="max-height:80px;">
 | 
			
		||||
                    <div style="height:210px;border:1px dashed #ccc; margin-top:10px;">
 | 
			
		||||
                      {% if instance.broker and instance.broker.company and instance.broker.company.signature %}
 | 
			
		||||
                        <img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="max-height:200px;">
 | 
			
		||||
                      {% elif template.company and template.company.signature %}
 | 
			
		||||
                        <img src="{{ template.company.signature.url }}" alt="امضای شرکت" style="max-height:200px;">
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -76,15 +88,23 @@
 | 
			
		|||
          <form method="post" class="d-flex justify-content-between mt-3">
 | 
			
		||||
            {% csrf_token %}
 | 
			
		||||
            {% if previous_step %}
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
 | 
			
		||||
              <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 %}
 | 
			
		||||
            {% if next_step %}
 | 
			
		||||
              {% if is_broker %}
 | 
			
		||||
                <button type="submit" class="btn btn-primary">تایید و بعدی</button>
 | 
			
		||||
                <button type="submit" class="btn btn-primary">تایید و بعدی
 | 
			
		||||
                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
              {% else %}
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
                بعدی
 | 
			
		||||
                <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
              </a>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
              {% if is_broker %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,43 +2,63 @@ from django.shortcuts import render, get_object_or_404, redirect
 | 
			
		|||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
from django.template import Template, Context
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from processes.models import ProcessInstance, StepInstance
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from .models import ContractTemplate, ContractInstance
 | 
			
		||||
from invoices.models import Invoice, Quote
 | 
			
		||||
from _helpers.utils import jalali_converter2
 | 
			
		||||
from django.http import JsonResponse
 | 
			
		||||
from processes.utils import get_scoped_instance_or_404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_contract_context(instance: ProcessInstance) -> dict:
 | 
			
		||||
    representative = instance.representative
 | 
			
		||||
    profile = getattr(representative, 'profile', None)
 | 
			
		||||
    well = instance.well
 | 
			
		||||
    # Compute prepayment from Quote-linked invoice payments
 | 
			
		||||
    quote = Quote.objects.filter(process_instance=instance).first()
 | 
			
		||||
    invoice = Invoice.objects.filter(quote=quote).first() if quote else None
 | 
			
		||||
    payments_qs = invoice.payments.filter(is_deleted=False, direction='in').all() if invoice else []
 | 
			
		||||
    total_paid = sum((p.amount for p in payments_qs), Decimal('0'))
 | 
			
		||||
    try:
 | 
			
		||||
        latest_payment_date = max((p.payment_date for p in payments_qs)) if payments_qs else None
 | 
			
		||||
    except Exception:
 | 
			
		||||
        latest_payment_date = None
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
        'customer_full_name': representative.get_full_name() if representative else '',
 | 
			
		||||
        'national_code': profile.national_code if profile else '',
 | 
			
		||||
        'address': profile.address if profile else '',
 | 
			
		||||
        'phone': profile.phone_number_1 if profile else '',
 | 
			
		||||
        'phone2': profile.phone_number_2 if profile else '',
 | 
			
		||||
        'water_subscription_number': well.water_subscription_number if well else '',
 | 
			
		||||
        'electricity_subscription_number': well.electricity_subscription_number if well else '',
 | 
			
		||||
        'water_meter_serial_number': well.water_meter_serial_number if well else '',
 | 
			
		||||
        'well_power': well.well_power if well else '',
 | 
			
		||||
        'request_code': instance.code,
 | 
			
		||||
        'today': jalali_converter2(timezone.now()),
 | 
			
		||||
        'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{representative.get_full_name() if representative else ''}</span>"),
 | 
			
		||||
        'registration_number': mark_safe(f"<span class=\"fw-bold\">{instance.broker.company.registration_number if instance.broker and instance.broker.company else ''}</span>"),
 | 
			
		||||
        'national_code': mark_safe(f"<span class=\"fw-bold\">{profile.national_code if profile else ''}</span>"),
 | 
			
		||||
        'address': mark_safe(f"<span class=\"fw-bold\">{profile.address if profile else ''}</span>"),
 | 
			
		||||
        'phone': mark_safe(f"<span class=\"fw-bold\">{profile.phone_number_1 if profile else ''}</span>"),
 | 
			
		||||
        'phone2': mark_safe(f"<span class=\"fw-bold\">{profile.phone_number_2 if profile else ''}</span>"),
 | 
			
		||||
        'water_subscription_number': mark_safe(f"<span class=\"fw-bold\">{well.water_subscription_number if well else ''}</span>"),
 | 
			
		||||
        'electricity_subscription_number': mark_safe(f"<span class=\"fw-bold\">{well.electricity_subscription_number if well else ''}</span>"),
 | 
			
		||||
        'water_meter_serial_number': mark_safe(f"<span class=\"fw-bold\">{well.water_meter_serial_number if well else ''}</span>"),
 | 
			
		||||
        'well_power': mark_safe(f"<span class=\"fw-bold\">{well.well_power if well else ''}</span>"),
 | 
			
		||||
        'request_code': mark_safe(f"<span class=\"fw-bold\">{instance.code}</span>"),
 | 
			
		||||
        'today': mark_safe(f"<span class=\"fw-bold\">{jalali_converter2(timezone.now())}</span>"),
 | 
			
		||||
        'company_name': mark_safe(f"<span class=\"fw-bold\">{instance.broker.company.name if instance.broker and instance.broker.company else ''}</span>"),
 | 
			
		||||
        'city_name': mark_safe(f"<span class=\"fw-bold\">{instance.broker.affairs.county.city.name if instance.broker and instance.broker.affairs and instance.broker.affairs.county and instance.broker.affairs.county.city else ''}</span>"),
 | 
			
		||||
        'card_number': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.card_number if instance.representative else ''}</span>"),
 | 
			
		||||
        'account_number': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.account_number if instance.representative else ''}</span>"),
 | 
			
		||||
        'bank_name': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.get_bank_name_display() if instance.representative else ''}</span>"),
 | 
			
		||||
        'prepayment_amount': mark_safe(f"<span class=\"fw-bold\">{int(total_paid):,}</span>"),
 | 
			
		||||
        'prepayment_date': mark_safe(f"<span class=\"fw-bold\">{jalali_converter2(latest_payment_date)}</span>") if latest_payment_date else '',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def contract_step(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    # Resolve step navigation
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    # Access control:
 | 
			
		||||
    # - INSTALLER: can open step but cannot view contract body (show inline message)
 | 
			
		||||
    # - Others: can view
 | 
			
		||||
    # - Only BROKER can submit/complete this step
 | 
			
		||||
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    can_view_contract_body = True
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +92,6 @@ def contract_step(request, instance_id, step_id):
 | 
			
		|||
    # If user submits to go next, only broker can complete and go to next
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        if not is_broker:
 | 
			
		||||
            from django.http import JsonResponse
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
			
		||||
        StepInstance.objects.update_or_create(
 | 
			
		||||
            process_instance=instance,
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +118,7 @@ def contract_step(request, instance_id, step_id):
 | 
			
		|||
 | 
			
		||||
@login_required
 | 
			
		||||
def contract_print(request, instance_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    contract = get_object_or_404(ContractInstance, process_instance=instance)
 | 
			
		||||
    return render(request, 'contracts/contract_print.html', {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -42,8 +42,8 @@ class InstallationReport(BaseModel):
 | 
			
		|||
    new_water_meter_serial = models.CharField(max_length=50, null=True, blank=True, verbose_name='سریال کنتور جدید')
 | 
			
		||||
    seal_number = models.CharField(max_length=50, null=True, blank=True, verbose_name='شماره پلمپ')
 | 
			
		||||
    is_meter_suspicious = models.BooleanField(default=False, verbose_name='کنتور مشکوک است؟')
 | 
			
		||||
    utm_x = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True, verbose_name='UTM X')
 | 
			
		||||
    utm_y = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True, verbose_name='UTM Y')
 | 
			
		||||
    utm_x = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, verbose_name='UTM X')
 | 
			
		||||
    utm_y = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, verbose_name='UTM Y')
 | 
			
		||||
    description = models.TextField(blank=True, verbose_name='توضیحات')
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='ایجادکننده')
 | 
			
		||||
    approved = models.BooleanField(default=False, verbose_name='تایید شده')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,12 @@
 | 
			
		|||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-12 mb-4">
 | 
			
		||||
| 
						 | 
				
			
			@ -30,11 +35,13 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
          <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
          بازگشت
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
			
		||||
| 
						 | 
				
			
			@ -64,17 +71,17 @@
 | 
			
		|||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if assignment.assigned_by or assignment.installer %}
 | 
			
		||||
            <div class="mt-3 border rounded p-3 bg-light">
 | 
			
		||||
            <div class="mt-3 alert alert-primary">
 | 
			
		||||
              <div class="row g-2">
 | 
			
		||||
                {% if assignment.assigned_by %}
 | 
			
		||||
                <div class="col-12 col-md-6">
 | 
			
		||||
                  <div class="small text-muted">تعیینکننده نصاب</div>
 | 
			
		||||
                  <div class="small text-dark">تعیینکننده نصاب</div>
 | 
			
		||||
                  <div>{{ assignment.assigned_by.get_full_name|default:assignment.assigned_by.username }} <span class="text-muted">({{ assignment.assigned_by.username }})</span></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if assignment.updated %}
 | 
			
		||||
                <div class="col-12 col-md-6">
 | 
			
		||||
                  <div class="small text-muted">تاریخ ثبت/ویرایش</div>
 | 
			
		||||
                  <div class="small text-dark">تاریخ ثبت/ویرایش</div>
 | 
			
		||||
                  <div>{{ assignment.updated|to_jalali }}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
| 
						 | 
				
			
			@ -83,14 +90,22 @@
 | 
			
		|||
            {% endif %}
 | 
			
		||||
            <div class="d-flex justify-content-between mt-4">
 | 
			
		||||
              {% if previous_step %}
 | 
			
		||||
                <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
 | 
			
		||||
                <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>
 | 
			
		||||
                  قبلی
 | 
			
		||||
                </a>
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <span></span>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
              {% if is_manager %}
 | 
			
		||||
                <button class="btn btn-primary" type="submit">ثبت و ادامه</button>
 | 
			
		||||
                <button class="btn btn-primary" type="submit">ثبت و ادامه
 | 
			
		||||
                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
              {% else %}
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
                بعدی
 | 
			
		||||
                <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
              </a>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
          </form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,12 @@
 | 
			
		|||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-12 mb-4">
 | 
			
		||||
| 
						 | 
				
			
			@ -43,11 +48,13 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
          <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
          بازگشت
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
			
		||||
| 
						 | 
				
			
			@ -55,17 +62,14 @@
 | 
			
		|||
        
 | 
			
		||||
        <div class="bs-stepper-content">
 | 
			
		||||
          {% if report and not edit_mode %}
 | 
			
		||||
          <div class="card mb-3 border">
 | 
			
		||||
            <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
              <div class="d-flex gap-2">
 | 
			
		||||
                {% if request.user|is_installer %}
 | 
			
		||||
                  <a href="?edit=1" class="btn btn-primary">ویرایش گزارش نصب</a>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                  <button type="button" class="btn btn-primary" disabled>ویرایش گزارش نصب</button>
 | 
			
		||||
          <div class="mb-3 text-end">
 | 
			
		||||
            {% if user_is_installer %}
 | 
			
		||||
              <a href="?edit=1" class="btn btn-primary">
 | 
			
		||||
                <i class="bx bx-edit bx-sm me-2"></i>
 | 
			
		||||
                ویرایش گزارش نصب
 | 
			
		||||
              </a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
          {% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %}
 | 
			
		||||
              <div class="alert alert-danger d-flex align-items-start" role="alert">
 | 
			
		||||
                <i class="bx bx-error-circle me-2"></i>
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +79,8 @@
 | 
			
		|||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
          <div class="card mb-3 border">
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
              <div class="row">
 | 
			
		||||
                <div class="col-md-6">
 | 
			
		||||
                  <p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
 | 
			
		||||
| 
						 | 
				
			
			@ -151,7 +157,7 @@
 | 
			
		|||
              <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
			
		||||
              {% if user_can_approve %}
 | 
			
		||||
              <div class="d-flex gap-2">
 | 
			
		||||
                <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal" {% if step_instance and step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
 | 
			
		||||
                <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal">تایید</button>
 | 
			
		||||
                <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
 | 
			
		||||
              </div>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
| 
						 | 
				
			
			@ -184,18 +190,28 @@
 | 
			
		|||
          <!-- Persistent nav in edit mode (outside cards) -->
 | 
			
		||||
          <div class="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">قبلی</a>
 | 
			
		||||
              <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>
 | 
			
		||||
                قبلی
 | 
			
		||||
              </a>
 | 
			
		||||
            {% else %}
 | 
			
		||||
              <span></span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if next_step %}
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
                بعدی
 | 
			
		||||
                <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
              </a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          </div>
 | 
			
		||||
          {% else %}
 | 
			
		||||
          {% if not request.user|is_installer %}
 | 
			
		||||
          <div class="alert alert-warning">شما مجوز ثبت/ویرایش گزارش نصب را ندارید. اطلاعات به صورت فقط خواندنی نمایش داده میشود.</div>
 | 
			
		||||
 | 
			
		||||
          {% if not user_is_installer %}
 | 
			
		||||
          <div class="alert alert-warning">شما مجوز ثبت/ویرایش گزارش نصب را ندارید.</div>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
 | 
			
		||||
          {% if user_is_installer %}
 | 
			
		||||
          <!-- Installation Report Form -->
 | 
			
		||||
          <form method="post" enctype="multipart/form-data" id="installation-report-form">
 | 
			
		||||
            {% csrf_token %}
 | 
			
		||||
            <div class="mb-3">
 | 
			
		||||
| 
						 | 
				
			
			@ -203,40 +219,40 @@
 | 
			
		|||
                <div class="row g-3">
 | 
			
		||||
                  <div class="col-md-3">
 | 
			
		||||
                    <label class="form-label">تاریخ مراجعه</label>
 | 
			
		||||
                    <input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if not request.user|is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
 | 
			
		||||
                    <input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if not user_is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
 | 
			
		||||
                    <input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% endif %}">
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-3">
 | 
			
		||||
                    <label class="form-label">سریال کنتور جدید</label>
 | 
			
		||||
                    <input type="text" class="form-control" name="new_water_meter_serial" value="{% if report and edit_mode %}{{ report.new_water_meter_serial|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
 | 
			
		||||
                    <input type="text" class="form-control" name="new_water_meter_serial" value="{% if report and edit_mode %}{{ report.new_water_meter_serial|default_if_none:'' }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-3">
 | 
			
		||||
                    <label class="form-label">شماره پلمپ</label>
 | 
			
		||||
                    <input type="text" class="form-control" name="seal_number" value="{% if report and edit_mode %}{{ report.seal_number|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
 | 
			
		||||
                    <input type="text" class="form-control" name="seal_number" value="{% if report and edit_mode %}{{ report.seal_number|default_if_none:'' }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-3 d-flex align-items-end">
 | 
			
		||||
                    <div class="form-check">
 | 
			
		||||
                      <input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious" {% if not request.user|is_installer %}disabled{% endif %} {% if report and edit_mode and report.is_meter_suspicious %}checked{% endif %}>
 | 
			
		||||
                      <input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious" {% if not user_is_installer %}disabled{% endif %} {% if report and edit_mode and report.is_meter_suspicious %}checked{% endif %}>
 | 
			
		||||
                      <label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-3">
 | 
			
		||||
                    <label class="form-label">UTM X</label>
 | 
			
		||||
                    <input type="number" step="0.000001" class="form-control" name="utm_x" value="{% if report and edit_mode and report.utm_x %}{{ report.utm_x }}{% elif instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
 | 
			
		||||
                    <input type="number" step="1" class="form-control" name="utm_x" value="{% if report and edit_mode and report.utm_x %}{{ report.utm_x }}{% elif instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-3">
 | 
			
		||||
                    <label class="form-label">UTM Y</label>
 | 
			
		||||
                    <input type="number" step="0.000001" class="form-control" name="utm_y" value="{% if report and edit_mode and report.utm_y %}{{ report.utm_y }}{% elif instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
 | 
			
		||||
                    <input type="number" step="1" class="form-control" name="utm_y" value="{% if report and edit_mode and report.utm_y %}{{ report.utm_y }}{% elif instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="my-3">
 | 
			
		||||
                  <label class="form-label">توضیحات (اختیاری)</label>
 | 
			
		||||
                  <textarea class="form-control" rows="3" name="description" {% if not request.user|is_installer %}readonly{% endif %}>{% if report and edit_mode %}{{ report.description|default_if_none:'' }}{% endif %}</textarea>
 | 
			
		||||
                  <textarea class="form-control" rows="3" name="description" {% if not user_is_installer %}readonly{% endif %}>{% if report and edit_mode %}{{ report.description|default_if_none:'' }}{% endif %}</textarea>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="mb-3">
 | 
			
		||||
                  <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <label class="form-label mb-0">عکسها</label>
 | 
			
		||||
                    {% if request.user|is_installer %}
 | 
			
		||||
                    {% if user_is_installer %}
 | 
			
		||||
                      <button type="button" class="btn btn-sm btn-outline-primary" id="btnAddPhoto"><i class="bx bx-plus"></i> افزودن عکس</button>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                  </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +262,7 @@
 | 
			
		|||
                      <div class="col-6 col-md-3 mb-2" id="existing-photo-{{ p.id }}">
 | 
			
		||||
                        <div class="position-relative border rounded p-1">
 | 
			
		||||
                          <img class="img-fluid rounded" src="{{ p.image.url }}" alt="photo">
 | 
			
		||||
                          {% if request.user|is_installer %}
 | 
			
		||||
                          {% if user_is_installer %}
 | 
			
		||||
                            <button type="button" class="btn btn-sm btn-danger position-absolute" style="top:6px; left:6px;" onclick="markDeletePhoto('{{ p.id }}')" title="حذف/برگردان"><i class="bx bx-trash"></i></button>
 | 
			
		||||
                          {% endif %}
 | 
			
		||||
                          <input type="hidden" name="del_photo_{{ p.id }}" id="del-photo-{{ p.id }}" value="0">
 | 
			
		||||
| 
						 | 
				
			
			@ -350,21 +366,25 @@
 | 
			
		|||
 | 
			
		||||
            </div>
 | 
			
		||||
          </form>
 | 
			
		||||
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          <div class="mt-3 d-flex justify-content-between">
 | 
			
		||||
            {% if previous_step %}
 | 
			
		||||
              <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
 | 
			
		||||
              <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>
 | 
			
		||||
                قبلی
 | 
			
		||||
              </a>
 | 
			
		||||
            {% else %}
 | 
			
		||||
              <span></span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <div class="d-flex gap-2">
 | 
			
		||||
              {% if request.user|is_installer %}
 | 
			
		||||
                <button type="submit" class="btn btn-primary" form="installation-report-form">ثبت گزارش</button>
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <button type="button" class="btn btn-primary" disabled>ثبت گزارش</button>
 | 
			
		||||
              {% if user_is_installer %}
 | 
			
		||||
                <button type="submit" class="btn btn-success" form="installation-report-form">ثبت گزارش</button>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
              {% if next_step %}
 | 
			
		||||
                <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-success">بعدی</a>
 | 
			
		||||
                <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
                  بعدی
 | 
			
		||||
                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -499,7 +519,6 @@
 | 
			
		|||
    try {
 | 
			
		||||
      if (sessionStorage.getItem('install_report_saved') === '1') {
 | 
			
		||||
        sessionStorage.removeItem('install_report_saved');
 | 
			
		||||
        showToast('گزارش نصب با موفقیت ثبت شد', 'success');
 | 
			
		||||
      }
 | 
			
		||||
    } catch(_) {}
 | 
			
		||||
  })();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,16 +10,17 @@ from accounts.models import Role
 | 
			
		|||
from invoices.models import Item, Quote, QuoteItem
 | 
			
		||||
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
 | 
			
		||||
from decimal import Decimal, InvalidOperation
 | 
			
		||||
from processes.utils import get_scoped_instance_or_404
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def installation_assign_step(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
 | 
			
		||||
    # Installers list (profiles that have installer role)
 | 
			
		||||
    installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value).select_related('user').all()
 | 
			
		||||
    installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value, county=instance.well.county).select_related('user').all()
 | 
			
		||||
    assignment, _ = InstallationAssignment.objects.get_or_create(process_instance=instance)
 | 
			
		||||
 | 
			
		||||
    # Role flags
 | 
			
		||||
| 
						 | 
				
			
			@ -72,17 +73,56 @@ def installation_assign_step(request, instance_id, step_id):
 | 
			
		|||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_item_changes_for_report(report, remove_map, add_map, quote_price_map):
 | 
			
		||||
    """Helper function to create item changes for a report"""
 | 
			
		||||
    # Create remove changes
 | 
			
		||||
    for item_id, qty in remove_map.items():
 | 
			
		||||
        up = quote_price_map.get(item_id)
 | 
			
		||||
        total = (up * qty) if up is not None else None
 | 
			
		||||
        InstallationItemChange.objects.create(
 | 
			
		||||
            report=report,
 | 
			
		||||
            item_id=item_id,
 | 
			
		||||
            change_type='remove',
 | 
			
		||||
            quantity=qty,
 | 
			
		||||
            unit_price=up,
 | 
			
		||||
            total_price=total,
 | 
			
		||||
        )
 | 
			
		||||
    
 | 
			
		||||
    # Create add changes
 | 
			
		||||
    for item_id, data in add_map.items():
 | 
			
		||||
        unit_price = data.get('price')
 | 
			
		||||
        qty = data.get('qty') or 1
 | 
			
		||||
        total = (unit_price * qty) if (unit_price is not None) else None
 | 
			
		||||
        InstallationItemChange.objects.create(
 | 
			
		||||
            report=report,
 | 
			
		||||
            item_id=item_id,
 | 
			
		||||
            change_type='add',
 | 
			
		||||
            quantity=qty,
 | 
			
		||||
            unit_price=unit_price,
 | 
			
		||||
            total_price=total,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def installation_report_step(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
 | 
			
		||||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
 | 
			
		||||
    assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
 | 
			
		||||
    existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first()
 | 
			
		||||
    # Only installers can enter edit mode
 | 
			
		||||
    user_is_installer = hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.INSTALLER)
 | 
			
		||||
 | 
			
		||||
    # Only the assigned installer can create/edit the report
 | 
			
		||||
    try:
 | 
			
		||||
        has_installer_role = bool(getattr(request.user, 'profile', None) and request.user.profile.has_role(UserRoles.INSTALLER))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        has_installer_role = False
 | 
			
		||||
    is_assigned_installer = bool(assignment and assignment.installer_id == request.user.id)
 | 
			
		||||
    user_is_installer = bool(has_installer_role and is_assigned_installer)
 | 
			
		||||
    edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False
 | 
			
		||||
 | 
			
		||||
    # current quote items baseline
 | 
			
		||||
    quote = Quote.objects.filter(process_instance=instance).first()
 | 
			
		||||
    quote_items = list(quote.items.select_related('item').all()) if quote else []
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +140,14 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
			
		||||
    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
			
		||||
    user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
 | 
			
		||||
    user_can_approve = any(r.role in user_roles for r in reqs)
 | 
			
		||||
    # Align permission check with invoices flow (role id intersection)
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = {r.role_id for r in reqs}
 | 
			
		||||
        user_role_ids = {ur.id for ur in user_roles}
 | 
			
		||||
        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
			
		||||
    except Exception:
 | 
			
		||||
        can_approve_reject = False
 | 
			
		||||
    user_can_approve = can_approve_reject
 | 
			
		||||
    approvals_list = list(step_instance.approvals.select_related('role').all())
 | 
			
		||||
    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
			
		||||
    approver_statuses = [
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +207,13 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
            existing_report.approved = False
 | 
			
		||||
            existing_report.save()
 | 
			
		||||
            # If current step moved ahead of this step, reset it back for correction (align with invoices)
 | 
			
		||||
            try:
 | 
			
		||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
                    instance.current_step = step
 | 
			
		||||
                    instance.save(update_fields=['current_step'])
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            messages.success(request, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.')
 | 
			
		||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -177,6 +231,21 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
        is_suspicious = True if request.POST.get('is_meter_suspicious') == 'on' else False
 | 
			
		||||
        utm_x = request.POST.get('utm_x') or None
 | 
			
		||||
        utm_y = request.POST.get('utm_y') or None
 | 
			
		||||
        # Normalize UTM to integer meters
 | 
			
		||||
        if utm_x is not None and utm_x != '':
 | 
			
		||||
            try:
 | 
			
		||||
                utm_x = int(Decimal(str(utm_x)))
 | 
			
		||||
            except InvalidOperation:
 | 
			
		||||
                utm_x = None
 | 
			
		||||
        else:
 | 
			
		||||
            utm_x = None
 | 
			
		||||
        if utm_y is not None and utm_y != '':
 | 
			
		||||
            try:
 | 
			
		||||
                utm_y = int(Decimal(str(utm_y)))
 | 
			
		||||
            except InvalidOperation:
 | 
			
		||||
                utm_y = None
 | 
			
		||||
        else:
 | 
			
		||||
            utm_y = None
 | 
			
		||||
 | 
			
		||||
        # Build maps from form fields: remove and add
 | 
			
		||||
        remove_map = {}
 | 
			
		||||
| 
						 | 
				
			
			@ -221,8 +290,6 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
                    unit_price = item_obj.unit_price if item_obj else None
 | 
			
		||||
                add_map[item_id] = {'qty': qty, 'price': unit_price}
 | 
			
		||||
 | 
			
		||||
        # اجازهٔ ثبت همزمان حذف و افزودن برای یک قلم (بدون محدودیت و ادغام)
 | 
			
		||||
 | 
			
		||||
        if existing_report and edit_mode:
 | 
			
		||||
            report = existing_report
 | 
			
		||||
            report.description = description
 | 
			
		||||
| 
						 | 
				
			
			@ -247,29 +314,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
                InstallationPhoto.objects.create(report=report, image=f)
 | 
			
		||||
            # replace item changes with new submission
 | 
			
		||||
            report.item_changes.all().delete()
 | 
			
		||||
            for item_id, qty in remove_map.items():
 | 
			
		||||
                up = quote_price_map.get(item_id)
 | 
			
		||||
                total = (up * qty) if up is not None else None
 | 
			
		||||
                InstallationItemChange.objects.create(
 | 
			
		||||
                    report=report,
 | 
			
		||||
                    item_id=item_id,
 | 
			
		||||
                    change_type='remove',
 | 
			
		||||
                    quantity=qty,
 | 
			
		||||
                    unit_price=up,
 | 
			
		||||
                    total_price=total,
 | 
			
		||||
                )
 | 
			
		||||
            for item_id, data in add_map.items():
 | 
			
		||||
                unit_price = data.get('price')
 | 
			
		||||
                qty = data.get('qty') or 1
 | 
			
		||||
                total = (unit_price * qty) if (unit_price is not None) else None
 | 
			
		||||
                InstallationItemChange.objects.create(
 | 
			
		||||
                    report=report,
 | 
			
		||||
                    item_id=item_id,
 | 
			
		||||
                    change_type='add',
 | 
			
		||||
                    quantity=qty,
 | 
			
		||||
                    unit_price=unit_price,
 | 
			
		||||
                    total_price=total,
 | 
			
		||||
                )
 | 
			
		||||
            create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
 | 
			
		||||
        else:
 | 
			
		||||
            report = InstallationReport.objects.create(
 | 
			
		||||
                assignment=assignment,
 | 
			
		||||
| 
						 | 
				
			
			@ -286,29 +331,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
            for f in request.FILES.getlist('photos'):
 | 
			
		||||
                InstallationPhoto.objects.create(report=report, image=f)
 | 
			
		||||
            # item changes
 | 
			
		||||
            for item_id, qty in remove_map.items():
 | 
			
		||||
                up = quote_price_map.get(item_id)
 | 
			
		||||
                total = (up * qty) if up is not None else None
 | 
			
		||||
                InstallationItemChange.objects.create(
 | 
			
		||||
                    report=report,
 | 
			
		||||
                    item_id=item_id,
 | 
			
		||||
                    change_type='remove',
 | 
			
		||||
                    quantity=qty,
 | 
			
		||||
                    unit_price=up,
 | 
			
		||||
                    total_price=total,
 | 
			
		||||
                )
 | 
			
		||||
            for item_id, data in add_map.items():
 | 
			
		||||
                unit_price = data.get('price')
 | 
			
		||||
                qty = data.get('qty') or 1
 | 
			
		||||
                total = (unit_price * qty) if (unit_price is not None) else None
 | 
			
		||||
                InstallationItemChange.objects.create(
 | 
			
		||||
                    report=report,
 | 
			
		||||
                    item_id=item_id,
 | 
			
		||||
                    change_type='add',
 | 
			
		||||
                    quantity=qty,
 | 
			
		||||
                    unit_price=unit_price,
 | 
			
		||||
                    total_price=total,
 | 
			
		||||
                )
 | 
			
		||||
            create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
 | 
			
		||||
 | 
			
		||||
        # After installer submits/edits, set step back to in_progress and clear approvals
 | 
			
		||||
        step_instance.status = 'in_progress'
 | 
			
		||||
| 
						 | 
				
			
			@ -319,6 +342,33 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        # If the report was edited, ensure downstream steps reopen like invoices flow
 | 
			
		||||
        try:
 | 
			
		||||
            subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
            for subsequent_step in subsequent_steps:
 | 
			
		||||
                subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
                if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                    # Reopen the step
 | 
			
		||||
                    instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                        status='in_progress',
 | 
			
		||||
                        completed_at=None
 | 
			
		||||
                    )
 | 
			
		||||
                    # Clear previous approvals if any
 | 
			
		||||
                    try:
 | 
			
		||||
                        subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        pass
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
        try:
 | 
			
		||||
            if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
                instance.current_step = step
 | 
			
		||||
                instance.save(update_fields=['current_step'])
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.')
 | 
			
		||||
        return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -340,6 +390,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
        'assignment': assignment,
 | 
			
		||||
        'report': existing_report,
 | 
			
		||||
        'edit_mode': edit_mode,
 | 
			
		||||
        'user_is_installer': user_is_installer,
 | 
			
		||||
        'quote': quote,
 | 
			
		||||
        'quote_items': quote_items,
 | 
			
		||||
        'all_items': items,
 | 
			
		||||
| 
						 | 
				
			
			@ -351,6 +402,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
			
		|||
        'step_instance': step_instance,
 | 
			
		||||
        'approver_statuses': approver_statuses,
 | 
			
		||||
        'user_can_approve': user_can_approve,
 | 
			
		||||
        'can_approve_reject': can_approve_reject,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ from decimal import Decimal
 | 
			
		|||
from django.utils import timezone
 | 
			
		||||
from django.core.validators import MinValueValidator
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from _helpers.utils import jalali_converter2
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +92,7 @@ class Quote(NameSlugModel):
 | 
			
		|||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        related_name='created_quotes'
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
| 
						 | 
				
			
			@ -371,3 +373,6 @@ class Payment(BaseModel):
 | 
			
		|||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def jpayment_date(self):
 | 
			
		||||
        return jalali_converter2(self.payment_date)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,55 +1,206 @@
 | 
			
		|||
{% extends '_base.html' %}
 | 
			
		||||
{% load humanize %}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="fa" dir="rtl">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>فاکتور نهایی {{ invoice.name }} - {{ instance.code }}</title>
 | 
			
		||||
  
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container py-4">
 | 
			
		||||
  <div class="mb-4 d-flex justify-content-between align-items-center">
 | 
			
		||||
    <div>
 | 
			
		||||
      <h4 class="mb-1">فاکتور نهایی</h4>
 | 
			
		||||
      <small class="text-muted">کد درخواست: {{ instance.code }}</small>
 | 
			
		||||
  {% load static %}
 | 
			
		||||
  {% load humanize %}
 | 
			
		||||
 | 
			
		||||
  <!-- Fonts (match base) -->
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
			
		||||
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
			
		||||
  <link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
 | 
			
		||||
 | 
			
		||||
  <!-- Icons (optional) -->
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/fonts/flag-icons.css' %}">
 | 
			
		||||
 | 
			
		||||
  <!-- Core CSS (same as preview) -->
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
 | 
			
		||||
  <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
			
		||||
  
 | 
			
		||||
  <style>
 | 
			
		||||
    @page {
 | 
			
		||||
      size: A4;
 | 
			
		||||
      margin: 1cm;
 | 
			
		||||
    }
 | 
			
		||||
    @media print {
 | 
			
		||||
      body { print-color-adjust: exact; }
 | 
			
		||||
      .page-break { page-break-before: always; }
 | 
			
		||||
      .no-print { display: none !important; }
 | 
			
		||||
    }
 | 
			
		||||
    .invoice-header { border-bottom: 1px solid #dee2e6; 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">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    <div class="invoice-header">
 | 
			
		||||
      <div class="row align-items-center">
 | 
			
		||||
        <div class="col-6 d-flex align-items-center">
 | 
			
		||||
          <div class="me-3" style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;background:#eef2ff;border-radius:8px;">
 | 
			
		||||
            {% if instance.broker.company and instance.broker.company.logo %}
 | 
			
		||||
              <img src="{{ instance.broker.company.logo.url }}" alt="لوگو" style="max-height:58px;max-width:120px;">
 | 
			
		||||
            {% else %}
 | 
			
		||||
              <span class="company-logo">شرکت</span>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
      <!-- Placeholders for logo/signature -->
 | 
			
		||||
      <div class="text-end">لوگو</div>
 | 
			
		||||
            {% if instance.broker.company %}
 | 
			
		||||
              {{ instance.broker.company.name }}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if instance.broker.company %}
 | 
			
		||||
              <div class="text-muted small">
 | 
			
		||||
                {% if instance.broker.company.address %}
 | 
			
		||||
                  <div>{{ instance.broker.company.address }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.broker.affairs.county.city.name %}
 | 
			
		||||
                  <div>{{ instance.broker.affairs.county.city.name }}، ایران</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.broker.company.phone %}
 | 
			
		||||
                  <div>تلفن: {{ instance.broker.company.phone }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
              </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
  <div class="table-responsive">
 | 
			
		||||
    <table class="table table-bordered">
 | 
			
		||||
        <div class="col-6 text-end">
 | 
			
		||||
          <div class="mt-2">
 | 
			
		||||
            <div><strong>#فاکتور نهایی {{ instance.code }}</strong></div>
 | 
			
		||||
            <div class="text-muted small">تاریخ صدور: {{ invoice.jcreated_date }}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Customer & Well Info -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
      <div class="col-6">
 | 
			
		||||
        <h6 class="fw-bold mb-2">اطلاعات مشترک</h6>
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">نام:</span> {{ invoice.customer.get_full_name|default:instance.representative.get_full_name }}</div>
 | 
			
		||||
        {% if instance.representative.profile and instance.representative.profile.national_code %}
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if instance.representative.profile and instance.representative.profile.address %}
 | 
			
		||||
        <div class="small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-6">
 | 
			
		||||
        <h6 class="fw-bold mb-2">اطلاعات چاه</h6>
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">شماره اشتراک برق:</span> {{ instance.well.electricity_subscription_number|default:"-" }}</div>
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">سریال کنتور:</span> {{ instance.well.water_meter_serial_number|default:"-" }}</div>
 | 
			
		||||
        <div class="small"><span class="text-muted">قدرت چاه:</span> {{ instance.well.well_power|default:"-" }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Items Table -->
 | 
			
		||||
    <div class="mb-4">
 | 
			
		||||
      <table class="table border-top m-0 items-table">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
          <th>آیتم</th>
 | 
			
		||||
          <th>تعداد</th>
 | 
			
		||||
          <th>قیمت واحد</th>
 | 
			
		||||
          <th>قیمت کل</th>
 | 
			
		||||
            <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 it in items %}
 | 
			
		||||
          <tr>
 | 
			
		||||
          <td>{{ it.item.name }}</td>
 | 
			
		||||
            <td>{{ forloop.counter }}</td>
 | 
			
		||||
            <td class="text-nowrap">{{ it.item.name }}</td>
 | 
			
		||||
            <td class="text-nowrap">{{ it.item.description|default:"-" }}</td>
 | 
			
		||||
            <td>{{ it.quantity }}</td>
 | 
			
		||||
            <td>{{ it.unit_price|floatformat:0|intcomma:False }}</td>
 | 
			
		||||
            <td>{{ it.total_price|floatformat:0|intcomma:False }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% empty %}
 | 
			
		||||
        <tr><td colspan="4" class="text-center text-muted">آیتمی ندارد</td></tr>
 | 
			
		||||
          <tr><td colspan="6" class="text-center text-muted">آیتمی ندارد</td></tr>
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
        <tfoot>
 | 
			
		||||
        <tr><th colspan="3" class="text-end">مبلغ کل</th><th>{{ invoice.total_amount|floatformat:0|intcomma:False }}</th></tr>
 | 
			
		||||
        <tr><th colspan="3" class="text-end">تخفیف</th><th>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</th></tr>
 | 
			
		||||
        <tr><th colspan="3" class="text-end">مبلغ نهایی</th><th>{{ invoice.final_amount|floatformat:0|intcomma:False }}</th></tr>
 | 
			
		||||
        <tr><th colspan="3" class="text-end">پرداختیها</th><th>{{ invoice.paid_amount|floatformat:0|intcomma:False }}</th></tr>
 | 
			
		||||
        <tr><th colspan="3" class="text-end">مانده</th><th>{{ invoice.remaining_amount|floatformat:0|intcomma:False }}</th></tr>
 | 
			
		||||
          <tr class="total-section">
 | 
			
		||||
            <td colspan="5" class="text-end"><strong>جمع کل(تومان):</strong></td>
 | 
			
		||||
            <td><strong>{{ invoice.total_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% if invoice.discount_amount > 0 %}
 | 
			
		||||
          <tr class="total-section">
 | 
			
		||||
            <td colspan="5" class="text-end"><strong>تخفیف(تومان):</strong></td>
 | 
			
		||||
            <td><strong>{{ invoice.discount_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          <tr class="total-section border-top border-2">
 | 
			
		||||
            <td colspan="5" class="text-end"><strong>مبلغ نهایی(تومان):</strong></td>
 | 
			
		||||
            <td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr class="total-section">
 | 
			
		||||
            <td colspan="5" class="text-end"><strong>پرداختیها(تومان):</strong></td>
 | 
			
		||||
            <td><strong">{{ invoice.paid_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr class="total-section">
 | 
			
		||||
            <td colspan="5" class="text-end"><strong>مانده(تومان):</strong></td>
 | 
			
		||||
            <td><strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tfoot>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
  <div class="mt-5 d-flex justify-content-between">
 | 
			
		||||
    <div>امضا مشتری</div>
 | 
			
		||||
    <div>امضا شرکت</div>
 | 
			
		||||
 | 
			
		||||
    <!-- Conditions & Payment -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="col-8">
 | 
			
		||||
        <h6 class="fw-bold">مهر و امضا:</h6>
 | 
			
		||||
        <ul class="small mb-0">
 | 
			
		||||
          {% if instance.broker.company and instance.broker.company.signature %}
 | 
			
		||||
          <li class="mt-3" style="list-style:none;"><img src="{{ instance.broker.company.signature.url }}" alt="امضا" style="height: 200px;"></li>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
      {% if instance.broker.company %}
 | 
			
		||||
      <div class="col-4">
 | 
			
		||||
        <h6 class="fw-bold mb-2">اطلاعات پرداخت</h6>
 | 
			
		||||
        {% if instance.broker.company.card_number %}
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">شماره کارت:</span> {{ instance.broker.company.card_number }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if instance.broker.company.account_number %}
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">شماره حساب:</span> {{ instance.broker.company.account_number }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if instance.broker.company.sheba_number %}
 | 
			
		||||
        <div class="small mb-1"><span class="text-muted">شماره شبا:</span> {{ instance.broker.company.sheba_number }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if instance.broker.company.bank_name %}
 | 
			
		||||
        <div class="small"><span class="text-muted">بانک:</span> {{ instance.broker.company.get_bank_name_display }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
      {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<script>window.print()</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
    window.onload = function() {
 | 
			
		||||
      window.print();
 | 
			
		||||
      setTimeout(function(){ window.close(); }, 200);
 | 
			
		||||
    };
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,10 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
| 
						 | 
				
			
			@ -32,14 +36,18 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="d-flex gap-2">
 | 
			
		||||
        <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت</a>
 | 
			
		||||
        <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
			
		||||
          <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -163,15 +171,24 @@
 | 
			
		|||
        </div>
 | 
			
		||||
        <div class="card-footer d-flex justify-content-between">
 | 
			
		||||
          {% if previous_step %}
 | 
			
		||||
            <a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
 | 
			
		||||
            <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>
 | 
			
		||||
              قبلی
 | 
			
		||||
            </a>
 | 
			
		||||
          {% else %}
 | 
			
		||||
            <span></span>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          {% if next_step %}
 | 
			
		||||
            {% if is_manager %}
 | 
			
		||||
              <button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button>
 | 
			
		||||
              <button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">
 | 
			
		||||
                تایید و ادامه
 | 
			
		||||
                <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
              </button>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
 | 
			
		||||
            <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
              بعدی
 | 
			
		||||
              <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,10 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
| 
						 | 
				
			
			@ -31,14 +35,18 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="d-flex gap-2">
 | 
			
		||||
          <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت</a>
 | 
			
		||||
          <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
          </a>
 | 
			
		||||
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +96,7 @@
 | 
			
		|||
                  <input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                  <button type="button" id="btnAddFinalPayment" class="btn btn-primary">افزودن</button>
 | 
			
		||||
                  <button type="button" id="btnAddFinalPayment" class="btn btn-primary">افزودن فیش/چک</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +158,7 @@
 | 
			
		|||
                  <tr>
 | 
			
		||||
                    <td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
 | 
			
		||||
                    <td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
 | 
			
		||||
                    <td>{{ p.payment_date|date:'Y/m/d' }}</td>
 | 
			
		||||
                    <td>{{ p.jpayment_date }}</td>
 | 
			
		||||
                    <td>{{ p.get_payment_method_display }}</td>
 | 
			
		||||
                    <td>{{ p.reference_number|default:'-' }}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +190,7 @@
 | 
			
		|||
          <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
			
		||||
          {% if can_approve_reject %}
 | 
			
		||||
          <div class="d-flex gap-2">
 | 
			
		||||
            <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
 | 
			
		||||
            <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal">تایید</button>
 | 
			
		||||
            <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button>
 | 
			
		||||
          </div>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
| 
						 | 
				
			
			@ -214,13 +222,19 @@
 | 
			
		|||
      {% endif %}
 | 
			
		||||
      <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">قبلی</a>
 | 
			
		||||
          <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>
 | 
			
		||||
            قبلی
 | 
			
		||||
          </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">بعدی</a>
 | 
			
		||||
            <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
              بعدی
 | 
			
		||||
              <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
            </a>
 | 
			
		||||
          {% else %}
 | 
			
		||||
            <a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
| 
						 | 
				
			
			@ -264,8 +278,8 @@
 | 
			
		|||
        <div class="modal-body">
 | 
			
		||||
          {% if invoice.remaining_amount != 0 %}
 | 
			
		||||
            <div class="alert alert-warning" role="alert">
 | 
			
		||||
              مانده فاکتور صفر نیست: <strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
 | 
			
		||||
              تا صفر نشود امکان تایید نیست.
 | 
			
		||||
              مانده فاکتور: <strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
 | 
			
		||||
              امکان تایید تا تسویه کامل فاکتور وجود ندارد.
 | 
			
		||||
            </div>
 | 
			
		||||
          {% else %}
 | 
			
		||||
            آیا از تایید این مرحله اطمینان دارید؟
 | 
			
		||||
| 
						 | 
				
			
			@ -316,11 +330,32 @@
 | 
			
		|||
  (function initPersianDatePicker(){
 | 
			
		||||
    if (window.$ && $.fn.persianDatepicker && $('#id_payment_date').length) {
 | 
			
		||||
      $('#id_payment_date').persianDatepicker({
 | 
			
		||||
        format: 'YYYY/MM/DD', initialValue: false, autoClose: true, persianDigit: false, observer: true,
 | 
			
		||||
        format: 'YYYY/MM/DD', 
 | 
			
		||||
        initialValue: false, 
 | 
			
		||||
        autoClose: true, 
 | 
			
		||||
        persianDigit: false, 
 | 
			
		||||
        observer: true,
 | 
			
		||||
        calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
 | 
			
		||||
        onSelect: function(unix){
 | 
			
		||||
          const g = new window.persianDate(unix).toCalendar('gregorian').format('YYYY-MM-DD');
 | 
			
		||||
          $('#id_payment_date').attr('data-gregorian', g);
 | 
			
		||||
          // تبدیل تاریخ شمسی به میلادی برای ارسال به سرور
 | 
			
		||||
          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 {
 | 
			
		||||
            // اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
 | 
			
		||||
            $('#id_payment_date').val(gregorianDateString);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          // ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
 | 
			
		||||
          $('#id_payment_date').attr('data-gregorian', gregorianDateString);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -328,8 +363,14 @@
 | 
			
		|||
 | 
			
		||||
  function buildForm(){
 | 
			
		||||
    const fd = new FormData(document.getElementById('formFinalPayment'));
 | 
			
		||||
    const g = document.getElementById('id_payment_date').getAttribute('data-gregorian');
 | 
			
		||||
    if (g) { fd.set('payment_date', g); }
 | 
			
		||||
    
 | 
			
		||||
    // تبدیل تاریخ شمسی به میلادی برای ارسال
 | 
			
		||||
    const persianDateValue = $('#id_payment_date').val();
 | 
			
		||||
    const gregorianDateValue = $('#id_payment_date').attr('data-gregorian');
 | 
			
		||||
    if (persianDateValue && gregorianDateValue) {
 | 
			
		||||
      fd.set('payment_date', gregorianDateValue);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return fd;
 | 
			
		||||
  }
 | 
			
		||||
  (function(){
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,15 +18,16 @@
 | 
			
		|||
<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' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
| 
						 | 
				
			
			@ -35,19 +36,21 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </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> پرینت
 | 
			
		||||
            <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></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">
 | 
			
		||||
      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
			
		||||
        {% stepper_header instance step %}
 | 
			
		||||
        <div class="bs-stepper-content">
 | 
			
		||||
      
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +63,7 @@
 | 
			
		|||
              </div>
 | 
			
		||||
              
 | 
			
		||||
              <div class="row g-3">
 | 
			
		||||
                {% if can_manage_payments %}
 | 
			
		||||
                {% if is_broker %}
 | 
			
		||||
                <div class="col-12 col-lg-5">
 | 
			
		||||
                  <div class="card h-100 border">
 | 
			
		||||
                    <div class="card-header">
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +107,7 @@
 | 
			
		|||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                <div class="col-12 {% if can_manage_payments %}col-lg-7{% else %}col-lg-12{% endif %}">
 | 
			
		||||
                <div class="col-12 {% if is_broker %}col-lg-7{% else %}col-lg-12{% endif %}">
 | 
			
		||||
                  <div class="card mb-3 border">
 | 
			
		||||
                    <div class="card-header d-flex justify-content-between">
 | 
			
		||||
                        <h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +164,7 @@
 | 
			
		|||
                          {% for p in payments %}
 | 
			
		||||
                          <tr>
 | 
			
		||||
                            <td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
 | 
			
		||||
                            <td>{{ p.payment_date|date:'Y/m/d' }}</td>
 | 
			
		||||
                            <td>{{ p.jpayment_date }}</td>
 | 
			
		||||
                            <td>{{ p.get_payment_method_display }}</td>
 | 
			
		||||
                            <td>{{ p.reference_number|default:'-' }}</td>
 | 
			
		||||
                            <td>
 | 
			
		||||
| 
						 | 
				
			
			@ -171,7 +174,7 @@
 | 
			
		|||
                                  <i class="bx bx-show"></i>
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                                {% if can_manage_payments %}
 | 
			
		||||
                                {% if is_broker %}
 | 
			
		||||
                                <button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف">
 | 
			
		||||
                                  <i class="bx bx-trash"></i>
 | 
			
		||||
                                </button>
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +198,7 @@
 | 
			
		|||
                      <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
			
		||||
                      {% if can_approve_reject %}
 | 
			
		||||
                      <div class="d-flex gap-2">
 | 
			
		||||
                        <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approvePaymentsModal2" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
 | 
			
		||||
                        <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approvePaymentsModal2">تایید</button>
 | 
			
		||||
                        <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
| 
						 | 
				
			
			@ -356,6 +359,13 @@
 | 
			
		|||
    }
 | 
			
		||||
    const form = document.getElementById('formAddPayment');
 | 
			
		||||
    const fd = buildFormData(form);
 | 
			
		||||
    
 | 
			
		||||
    // تبدیل تاریخ شمسی به میلادی برای ارسال
 | 
			
		||||
    const persianDateValue = $('#id_payment_date').val();
 | 
			
		||||
    const gregorianDateValue = $('#id_payment_date').attr('data-gregorian');
 | 
			
		||||
    if (persianDateValue && gregorianDateValue) {
 | 
			
		||||
      fd.set('payment_date', gregorianDateValue);
 | 
			
		||||
    }
 | 
			
		||||
    fetch('{% url "invoices:add_quote_payment" instance.id step.id %}', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: fd
 | 
			
		||||
| 
						 | 
				
			
			@ -419,18 +429,24 @@
 | 
			
		|||
          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 {
 | 
			
		||||
              // اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
 | 
			
		||||
              $('#id_payment_date').val(gregorianDateString);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
 | 
			
		||||
            $('#id_payment_date').attr('data-gregorian', gregorianDateString);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,10 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
{% csrf_token %}
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
| 
						 | 
				
			
			@ -32,15 +36,17 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </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> پرینت
 | 
			
		||||
            <i class="bx bx-printer me-2"></i> پرینت
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,100 +56,116 @@
 | 
			
		|||
            <!-- Invoice Preview Card -->
 | 
			
		||||
      <div class="card invoice-preview-card mt-4 border">
 | 
			
		||||
        <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="d-flex justify-content-between flex-xl-row flex-md-column flex-sm-row flex-column p-sm-3 p-0 align-items-center">
 | 
			
		||||
            <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>
 | 
			
		||||
              <!-- Company Logo & Info -->
 | 
			
		||||
              <div class="d-flex align-items-center">
 | 
			
		||||
                <div class="avatar avatar-lg me-3">
 | 
			
		||||
                  <span class="avatar-initial rounded bg-label-primary">
 | 
			
		||||
                    {% if instance.broker.company %}
 | 
			
		||||
                      <img src="{{ instance.broker.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                      <i class="bx bx-buildings bx-md"></i>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                  </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>
 | 
			
		||||
                  <h5 class="mb-1">
 | 
			
		||||
                    {% if instance.broker.company %}
 | 
			
		||||
                      {{ instance.broker.company.name }}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                      شرکت آب منطقهای
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                  </h5>
 | 
			
		||||
                  {% if instance.broker.company %}
 | 
			
		||||
                    <div class="text-muted small">
 | 
			
		||||
                      {% if instance.broker.company.address %}
 | 
			
		||||
                        <div><i class="bx bx-map me-1"></i>{{ instance.broker.company.address }}</div>
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
                      {% if instance.broker.affairs.county.city.name %}
 | 
			
		||||
                        <div><i class="bx bx-current-location me-1"></i>{{ instance.broker.affairs.county.city.name }}، ایران</div>
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
                      {% if instance.broker.company.phone %}
 | 
			
		||||
                        <div><i class="bx bx-phone me-1"></i>{{ instance.broker.company.phone }}</div>
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- Invoice Details -->
 | 
			
		||||
            <div class="text-center">
 | 
			
		||||
              <div class="mb-3">
 | 
			
		||||
                <h5 class="text-body">#{{ quote.name }}</h5>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="invoice-details">
 | 
			
		||||
                <div class="d-flex justify-content-end align-items-center mb-2">
 | 
			
		||||
                  <span class="text-muted me-2">تاریخ صدور:</span>
 | 
			
		||||
                  <span class="fw-medium text-body">{{ quote.jcreated_date }}</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 class="card-body py-1">
 | 
			
		||||
          <div class="row">
 | 
			
		||||
            <div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
 | 
			
		||||
              <div class="">
 | 
			
		||||
                <div class="card-body p-3">
 | 
			
		||||
                  <h6 class="card-title text-primary mb-2">
 | 
			
		||||
                    <i class="bx bx-user me-1"></i>اطلاعات مشترک
 | 
			
		||||
                  </h6>
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">نام:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% if instance.representative.profile.national_code %}
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">کد ملی:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.representative.profile.national_code }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.representative.profile.phone_number_1 %}
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">تلفن:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.representative.profile.phone_number_1 }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.representative.profile.address %}
 | 
			
		||||
                  <div class="d-flex gap-2">
 | 
			
		||||
                    <span class="text-muted small">آدرس:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.representative.profile.address }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
 | 
			
		||||
              <div class="border-0 bg-light">
 | 
			
		||||
                <div class="card-body p-3">
 | 
			
		||||
                  <h6 class="card-title text-primary mb-2">
 | 
			
		||||
                    <i class="bx bx-droplet me-1"></i>اطلاعات چاه
 | 
			
		||||
                  </h6>
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">شماره اشتراک آب:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.well.water_subscription_number }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">شماره اشتراک برق:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.well.electricity_subscription_number|default:"-" }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="d-flex gap-2 mb-1">
 | 
			
		||||
                    <span class="text-muted small">سریال کنتور:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.well.water_meter_serial_number|default:"-" }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="d-flex gap-2">
 | 
			
		||||
                    <span class="text-muted small">قدرت چاه:</span>
 | 
			
		||||
                    <span class="fw-medium small">{{ instance.well.well_power|default:"-" }}</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </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>
 | 
			
		||||
| 
						 | 
				
			
			@ -170,11 +192,6 @@
 | 
			
		|||
              {% 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>
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +210,72 @@
 | 
			
		|||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
          <!-- Footer Information -->
 | 
			
		||||
          <div class="card-body border-top">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <div class="col-md-8">
 | 
			
		||||
                <h6 class="mb-3">شرایط و ضوابط:</h6>
 | 
			
		||||
                <ul class="list-unstyled mb-0">
 | 
			
		||||
                  <li class="mb-2">
 | 
			
		||||
                    <i class="bx bx-time-five text-muted me-2"></i>
 | 
			
		||||
                    اعتبار پیشفاکتور صادر شده ۴۸ ساعت پس از تاریخ صدور میباشد
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li class="mb-2">
 | 
			
		||||
                    <i class="bx bx-money text-muted me-2"></i>
 | 
			
		||||
                    مبلغ فوق به صورت علیالحساب دریافت میگردد
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li class="mb-0">
 | 
			
		||||
                    <i class="bx bx-info-circle text-muted me-2"></i>
 | 
			
		||||
                    این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد
 | 
			
		||||
                  </li>
 | 
			
		||||
                  {% if instance.broker.company.signature %}
 | 
			
		||||
                  <li class="mb-0 text-start mt-4 ms-5">
 | 
			
		||||
                    <img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="height: 200px;">
 | 
			
		||||
                  </li>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </ul>
 | 
			
		||||
              </div>
 | 
			
		||||
              {% if instance.broker.company %}
 | 
			
		||||
              <div class="col-md-4">
 | 
			
		||||
                <h6 class="mb-3">اطلاعات پرداخت:</h6>
 | 
			
		||||
                <div class="d-flex flex-column gap-2">
 | 
			
		||||
                  {% if instance.broker.company.card_number %}
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <small class="text-muted">شماره کارت:</small>
 | 
			
		||||
                    <div class="fw-medium">{{ instance.broker.company.card_number }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.broker.company.account_number %}
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <small class="text-muted">شماره حساب:</small>
 | 
			
		||||
                    <div class="fw-medium">{{ instance.broker.company.account_number }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.broker.company.sheba_number %}
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <small class="text-muted">شماره شبا:</small>
 | 
			
		||||
                    <div class="fw-medium">{{ instance.broker.company.sheba_number }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.broker.company.bank_name %}
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <small class="text-muted">بانک:</small>
 | 
			
		||||
                    <div class="fw-medium">{{ instance.broker.company.get_bank_name_display }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.broker.company.branch_name %}
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <small class="text-muted">شعبه:</small>
 | 
			
		||||
                    <div class="fw-medium">{{ instance.broker.company.branch_name }}</div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
              </div>
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {% if quote.notes %}
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +323,7 @@
 | 
			
		|||
            {% else %}
 | 
			
		||||
              {% if next_step %}
 | 
			
		||||
                <a href="{% url 'processes:step_detail' instance.id next_step.id %}" 
 | 
			
		||||
                   class="btn btn-label-primary">
 | 
			
		||||
                   class="btn btn-primary">
 | 
			
		||||
                  مرحله بعد
 | 
			
		||||
                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,24 @@
 | 
			
		|||
    <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">
 | 
			
		||||
    {% load static %}
 | 
			
		||||
    {% load humanize %}
 | 
			
		||||
 | 
			
		||||
    <!-- Fonts (match base) -->
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
			
		||||
    <link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
 | 
			
		||||
 | 
			
		||||
    <!-- Icons (optional) -->
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/vendor/fonts/flag-icons.css' %}">
 | 
			
		||||
 | 
			
		||||
    <!-- Core CSS (same as preview) -->
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
 | 
			
		||||
    <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
			
		||||
    
 | 
			
		||||
    <style>
 | 
			
		||||
        @page {
 | 
			
		||||
| 
						 | 
				
			
			@ -14,11 +30,7 @@
 | 
			
		|||
            margin: 1cm;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: 'Vazirmatn', sans-serif;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            line-height: 1.6;
 | 
			
		||||
        }
 | 
			
		||||
        /* Inherit project fonts and sizes from core.css/persian-fonts */
 | 
			
		||||
        
 | 
			
		||||
        @media print {
 | 
			
		||||
            body { print-color-adjust: exact; }
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +39,7 @@
 | 
			
		|||
        }
 | 
			
		||||
        
 | 
			
		||||
        .invoice-header {
 | 
			
		||||
            border-bottom: 2px solid #696cff;
 | 
			
		||||
            border-bottom: 1px solid #dee2e6;
 | 
			
		||||
            padding-bottom: 20px;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -89,195 +101,159 @@
 | 
			
		|||
</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>
 | 
			
		||||
        <!-- Auto print: buttons removed -->
 | 
			
		||||
        
 | 
			
		||||
        <!-- Invoice Header -->
 | 
			
		||||
        <!-- Invoice Header (compact, matches preview) -->
 | 
			
		||||
        <div class="invoice-header">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-6">
 | 
			
		||||
                    <div class="company-logo mb-3">
 | 
			
		||||
                        شرکت آب منطقهای
 | 
			
		||||
            <div class="row align-items-center">
 | 
			
		||||
                <div class="col-6 d-flex align-items-center">
 | 
			
		||||
                    <div class="me-3" style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;background:#eef2ff;border-radius:8px;">
 | 
			
		||||
                        {% if instance.broker.company and instance.broker.company.logo %}
 | 
			
		||||
                            <img src="{{ instance.broker.company.logo.url }}" alt="لوگو" style="max-height:58px;max-width:120px;">
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="company-logo">شرکت</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </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>
 | 
			
		||||
                        {% if instance.broker.company %}
 | 
			
		||||
                           {{ instance.broker.company.name }}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if instance.broker.company %}
 | 
			
		||||
                            <div class="text-muted small">
 | 
			
		||||
                                {% if instance.broker.company.address %}
 | 
			
		||||
                                    <div>{{ instance.broker.company.address }}</div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                                {% if instance.broker.affairs.county.city.name %}
 | 
			
		||||
                                    <div>{{ instance.broker.affairs.county.city.name }}، ایران</div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                                {% if instance.broker.company.phone %}
 | 
			
		||||
                                    <div>تلفن: {{ instance.broker.company.phone }}</div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </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 class="mt-2">
 | 
			
		||||
                        <div><strong>#{{ quote.name }}</strong></div>
 | 
			
		||||
                        <div class="text-muted small">تاریخ صدور: {{ quote.jcreated_date }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Customer & Well Info -->
 | 
			
		||||
        <div class="row mb-4">
 | 
			
		||||
        <!-- Customer & Well Info (compact to match preview) -->
 | 
			
		||||
        <div class="row mb-3">
 | 
			
		||||
            <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>
 | 
			
		||||
                <h6 class="fw-bold mb-2">اطلاعات مشترک</h6>
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">نام:</span> {{ quote.customer.get_full_name }}</div>
 | 
			
		||||
                {% if instance.representative.profile and instance.representative.profile.national_code %}
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.representative.profile and instance.representative.profile.address %}
 | 
			
		||||
                <div class="small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
 | 
			
		||||
                {% 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>
 | 
			
		||||
                <h6 class="fw-bold mb-2">اطلاعات چاه</h6>
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">شماره اشتراک برق:</span> {{ instance.well.electricity_subscription_number|default:"-" }}</div>
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">سریال کنتور:</span> {{ instance.well.water_meter_serial_number|default:"-" }}</div>
 | 
			
		||||
                <div class="small"><span class="text-muted">قدرت چاه:</span> {{ instance.well.well_power|default:"-" }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Items Table -->
 | 
			
		||||
        <div class="mb-4">
 | 
			
		||||
            <table class="table items-table">
 | 
			
		||||
            <table class="table border-top m-0">
 | 
			
		||||
                <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>
 | 
			
		||||
                        <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 class="text-nowrap">{{ quote_item.item.name }}</td>
 | 
			
		||||
                        <td class="text-nowrap">{{ 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>
 | 
			
		||||
                        <td>{{ quote_item.unit_price|floatformat:0|intcomma:False }}</td>
 | 
			
		||||
                        <td>{{ quote_item.total_price|floatformat:0|intcomma:False }}</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>
 | 
			
		||||
                        <td colspan="5" class="text-end"><strong>جمع کل(تومان):</strong></td>
 | 
			
		||||
                        <td><strong>{{ quote.total_amount|floatformat:0|intcomma:False }}</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>
 | 
			
		||||
                        <td colspan="5" class="text-end"><strong>تخفیف(تومان):</strong></td>
 | 
			
		||||
                        <td><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</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>
 | 
			
		||||
                        <td colspan="5" class="text-end"><strong>مبلغ نهایی(تومان):</strong></td>
 | 
			
		||||
                        <td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </tfoot>
 | 
			
		||||
            </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Notes -->
 | 
			
		||||
        {% if quote.notes %}
 | 
			
		||||
        <div class="mb-4">
 | 
			
		||||
            <h6 class="fw-bold">یادداشت:</h6>
 | 
			
		||||
            <p>{{ quote.notes }}</p>
 | 
			
		||||
        <!-- Conditions & Payment (matches preview) -->
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <div class="col-8">
 | 
			
		||||
                <h6 class="fw-bold mb-2">شرایط و ضوابط</h6>
 | 
			
		||||
                <ul class="small mb-0">
 | 
			
		||||
                    <li class="mb-1">اعتبار پیشفاکتور صادر شده ۴۸ ساعت پس از تاریخ صدور میباشد</li>
 | 
			
		||||
                    <li class="mb-1">مبلغ فوق به صورت علیالحساب دریافت میگردد</li>
 | 
			
		||||
                    <li class="mb-1">این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد</li>
 | 
			
		||||
                    {% if instance.broker.company and instance.broker.company.signature %}
 | 
			
		||||
                    <li class="mt-3" style="list-style:none;"><img src="{{ instance.broker.company.signature.url }}" alt="امضا" style="height: 200px;"></li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if instance.broker.company %}
 | 
			
		||||
            <div class="col-4">
 | 
			
		||||
                <h6 class="fw-bold mb-2">اطلاعات پرداخت</h6>
 | 
			
		||||
                {% if instance.broker.company.card_number %}
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">شماره کارت:</span> {{ instance.broker.company.card_number }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.broker.company.account_number %}
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">شماره حساب:</span> {{ instance.broker.company.account_number }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.broker.company.sheba_number %}
 | 
			
		||||
                <div class="small mb-1"><span class="text-muted">شماره شبا:</span> {{ instance.broker.company.sheba_number }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if instance.broker.company.bank_name %}
 | 
			
		||||
                <div class="small"><span class="text-muted">بانک:</span> {{ instance.broker.company.get_bank_name_display }}</div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </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>
 | 
			
		||||
        <!-- Signature Section (optional, compact) -->
 | 
			
		||||
        {% if quote.notes %}
 | 
			
		||||
        <div class="mt-3 small text-muted">یادداشت: {{ quote.notes }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        
 | 
			
		||||
        <!-- Footer -->
 | 
			
		||||
        <div class="text-center mt-4 small text-muted">
 | 
			
		||||
            این پیشفاکتور توسط سیستم مدیریت فرآیندها تولید شده است.
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <script>
 | 
			
		||||
        // Auto print on load (optional)
 | 
			
		||||
        // window.onload = function() { window.print(); }
 | 
			
		||||
        window.onload = function() {
 | 
			
		||||
            window.print();
 | 
			
		||||
            setTimeout(function(){ window.close(); }, 200);
 | 
			
		||||
        };
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,10 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-12 mb-4">
 | 
			
		||||
| 
						 | 
				
			
			@ -26,11 +30,13 @@
 | 
			
		|||
        <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:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
 | 
			
		||||
        <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
          <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="bs-stepper wizard-vertical vertical mt-2">
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +56,7 @@
 | 
			
		|||
                <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.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>
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +149,7 @@
 | 
			
		|||
                    {% endif %}
 | 
			
		||||
                  {% else %}
 | 
			
		||||
                    {% if next_step %}
 | 
			
		||||
                      <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-label-primary">
 | 
			
		||||
                      <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
			
		||||
                        مرحله بعد
 | 
			
		||||
                        <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
			
		||||
                      </a>
 | 
			
		||||
| 
						 | 
				
			
			@ -212,4 +218,6 @@
 | 
			
		|||
    }
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,9 +15,7 @@ urlpatterns = [
 | 
			
		|||
    # 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'),
 | 
			
		||||
| 
						 | 
				
			
			@ -33,5 +31,4 @@ urlpatterns = [
 | 
			
		|||
    path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/', views.final_settlement_step, name='final_settlement_step'),
 | 
			
		||||
    path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/add/', views.add_final_payment, name='add_final_payment'),
 | 
			
		||||
    path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/<int:payment_id>/delete/', views.delete_final_payment, name='delete_final_payment'),
 | 
			
		||||
    path('instance/<int:instance_id>/step/<int:step_id>/final-settlement/approve/', views.approve_final_settlement, name='approve_final_settlement'),
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,12 +12,17 @@ import json
 | 
			
		|||
from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval
 | 
			
		||||
from accounts.models import Role
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from .models import Item, Quote, QuoteItem, Payment, Invoice
 | 
			
		||||
from .models import Item, Quote, QuoteItem, Payment, Invoice, InvoiceItem
 | 
			
		||||
from installations.models import InstallationReport, InstallationItemChange
 | 
			
		||||
from processes.utils import get_scoped_instance_or_404
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def quote_step(request, instance_id, step_id):
 | 
			
		||||
    """مرحله انتخاب اقلام و ساخت پیشفاکتور"""
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_object_or_404(
 | 
			
		||||
        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
			
		||||
        id=instance_id
 | 
			
		||||
| 
						 | 
				
			
			@ -62,11 +67,12 @@ def quote_step(request, instance_id, step_id):
 | 
			
		|||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def create_quote(request, instance_id, step_id):
 | 
			
		||||
    """ساخت/بروزرسانی پیشفاکتور از اقلام انتخابی"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    # enforce permission: only BROKER can create/update quote
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +96,7 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        except Exception:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
    default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False).values_list('id', flat=True))
 | 
			
		||||
    default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False, is_special=False).values_list('id', flat=True))
 | 
			
		||||
    if default_item_ids:
 | 
			
		||||
        for default_id in default_item_ids:
 | 
			
		||||
            if default_id not in payload_by_id:
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +111,7 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
 | 
			
		||||
 | 
			
		||||
    # Create or reuse quote
 | 
			
		||||
    quote, _ = Quote.objects.get_or_create(
 | 
			
		||||
    quote, created_q = Quote.objects.get_or_create(
 | 
			
		||||
        process_instance=instance,
 | 
			
		||||
        defaults={
 | 
			
		||||
            'name': f"پیشفاکتور {instance.code}",
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +121,15 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Track whether this step was already completed before this edit
 | 
			
		||||
    step_instance_existing = instance.step_instances.filter(step=step).first()
 | 
			
		||||
    was_already_completed = bool(step_instance_existing and step_instance_existing.status == 'completed')
 | 
			
		||||
 | 
			
		||||
    # Snapshot previous items before overwrite for change detection
 | 
			
		||||
    previous_items_map = {}
 | 
			
		||||
    if not created_q:
 | 
			
		||||
        previous_items_map = {qi.item_id: int(qi.quantity) for qi in quote.items.filter(is_deleted=False).all()}
 | 
			
		||||
 | 
			
		||||
    # Replace quote items with submitted ones
 | 
			
		||||
    quote.items.all().delete()
 | 
			
		||||
    for entry in items_payload:
 | 
			
		||||
| 
						 | 
				
			
			@ -138,31 +153,81 @@ def create_quote(request, instance_id, step_id):
 | 
			
		|||
 | 
			
		||||
    quote.calculate_totals()
 | 
			
		||||
 | 
			
		||||
    # Detect changes versus previous state and mark audit fields if editing after completion
 | 
			
		||||
    try:
 | 
			
		||||
        new_items_map = {int(entry.get('id')): int(entry.get('qty') or 1) for entry in items_payload}
 | 
			
		||||
    except Exception:
 | 
			
		||||
        new_items_map = {}
 | 
			
		||||
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
 | 
			
		||||
    if was_already_completed and new_items_map != previous_items_map:
 | 
			
		||||
        # StepInstance-level generic audit (for reuse across steps)
 | 
			
		||||
        if step_instance_existing:
 | 
			
		||||
            step_instance_existing.edited_after_completion = True
 | 
			
		||||
            step_instance_existing.last_edited_at = timezone.now()
 | 
			
		||||
            step_instance_existing.last_edited_by = request.user
 | 
			
		||||
            step_instance_existing.edit_count = (step_instance_existing.edit_count or 0) + 1
 | 
			
		||||
            step_instance_existing.completed_at = timezone.now()
 | 
			
		||||
            step_instance_existing.save(update_fields=['edited_after_completion', 'last_edited_at', 'last_edited_by', 'edit_count', 'completed_at'])
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if quote.status != 'draft':
 | 
			
		||||
            quote.status = 'draft'
 | 
			
		||||
            quote.save(update_fields=['status'])
 | 
			
		||||
 | 
			
		||||
        # Reset ALL subsequent completed steps to in_progress
 | 
			
		||||
        subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
        for subsequent_step in subsequent_steps:
 | 
			
		||||
            subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
            if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                # Bypass validation by using update() instead of save()
 | 
			
		||||
                instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                    status='in_progress',
 | 
			
		||||
                    completed_at=None
 | 
			
		||||
                )
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
        # Set current step to the next step
 | 
			
		||||
        if next_step:
 | 
			
		||||
            instance.current_step = next_step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    
 | 
			
		||||
    # تکمیل مرحله
 | 
			
		||||
    step_instance, created = StepInstance.objects.get_or_create(
 | 
			
		||||
        process_instance=instance,
 | 
			
		||||
        step=step
 | 
			
		||||
    )
 | 
			
		||||
    if not was_already_completed:
 | 
			
		||||
        step_instance.status = 'completed'
 | 
			
		||||
        step_instance.completed_at = timezone.now()
 | 
			
		||||
    step_instance.save()
 | 
			
		||||
        step_instance.save(update_fields=['status', 'completed_at'])
 | 
			
		||||
    
 | 
			
		||||
    # انتقال به مرحله بعدی
 | 
			
		||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
			
		||||
    redirect_url = None
 | 
			
		||||
    if next_step:
 | 
			
		||||
        # Only advance current step if we are currently on this step to avoid regressions
 | 
			
		||||
        if instance.current_step_id == step.id:
 | 
			
		||||
            instance.current_step = next_step
 | 
			
		||||
        instance.save()
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
        # هدایت مستقیم به مرحله پیشنمایش پیشفاکتور
 | 
			
		||||
        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):
 | 
			
		||||
    """مرحله صدور پیشفاکتور - نمایش و تایید فاکتور"""
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
 | 
			
		||||
    instance = get_object_or_404(
 | 
			
		||||
        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
			
		||||
        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile', 'broker', 'broker__company', 'broker__affairs', 'broker__affairs__county', 'broker__affairs__county__city'),
 | 
			
		||||
        id=instance_id
 | 
			
		||||
    )
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -199,10 +264,11 @@ def quote_preview_step(request, instance_id, step_id):
 | 
			
		|||
        'is_broker': is_broker,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def quote_print(request, instance_id):
 | 
			
		||||
    """صفحه پرینت پیشفاکتور"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    quote = get_object_or_404(Quote, process_instance=instance)
 | 
			
		||||
    
 | 
			
		||||
    return render(request, 'invoices/quote_print.html', {
 | 
			
		||||
| 
						 | 
				
			
			@ -210,11 +276,12 @@ def quote_print(request, instance_id):
 | 
			
		|||
        'quote': quote,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required  
 | 
			
		||||
def approve_quote(request, instance_id, step_id):
 | 
			
		||||
    """تایید پیشفاکتور و انتقال به مرحله بعدی"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    quote = get_object_or_404(Quote, process_instance=instance)
 | 
			
		||||
    # enforce permission: only BROKER can approve
 | 
			
		||||
| 
						 | 
				
			
			@ -256,6 +323,9 @@ def approve_quote(request, instance_id, step_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def quote_payment_step(request, instance_id, step_id):
 | 
			
		||||
    """مرحله سوم: ثبت فیشهای واریزی پیشفاکتور"""
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
 | 
			
		||||
    instance = get_object_or_404(
 | 
			
		||||
        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
			
		||||
        id=instance_id
 | 
			
		||||
| 
						 | 
				
			
			@ -282,6 +352,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
 | 
			
		||||
 | 
			
		||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
			
		||||
    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
			
		||||
    user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
 | 
			
		||||
| 
						 | 
				
			
			@ -295,6 +366,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
        for r in reqs
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = {r.role_id for r in reqs}
 | 
			
		||||
| 
						 | 
				
			
			@ -302,20 +374,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
			
		||||
    except Exception:
 | 
			
		||||
        can_approve_reject = False
 | 
			
		||||
    # approver status map for template
 | 
			
		||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
			
		||||
    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
			
		||||
    user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
 | 
			
		||||
    approvals_list = list(step_instance.approvals.select_related('role').all())
 | 
			
		||||
    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
			
		||||
    approver_statuses = [
 | 
			
		||||
        {
 | 
			
		||||
            'role': r.role,
 | 
			
		||||
            'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
 | 
			
		||||
            'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
 | 
			
		||||
        }
 | 
			
		||||
        for r in reqs
 | 
			
		||||
    ]
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    # Accountant/Admin approval and rejection via POST (multi-role)
 | 
			
		||||
    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +418,13 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
			
		||||
            )
 | 
			
		||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
            # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
            try:
 | 
			
		||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
                    instance.current_step = step
 | 
			
		||||
                    instance.save(update_fields=['current_step'])
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
 | 
			
		||||
            return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -385,8 +451,6 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
        'approver_statuses': approver_statuses,
 | 
			
		||||
        'is_broker': is_broker,
 | 
			
		||||
        'is_accountant': is_accountant,
 | 
			
		||||
        # dynamic permissions: any role required to approve can also manage payments
 | 
			
		||||
        'can_manage_payments': can_approve_reject,
 | 
			
		||||
        'can_approve_reject': can_approve_reject,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -395,7 +459,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def add_quote_payment(request, instance_id, step_id):
 | 
			
		||||
    """افزودن فیش واریزی جدید برای پیشفاکتور"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, 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(
 | 
			
		||||
| 
						 | 
				
			
			@ -409,14 +473,16 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # dynamic permission: users whose roles are among required approvers can add payments
 | 
			
		||||
    # who can add payments
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    is_accountant = False
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
 | 
			
		||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
			
		||||
        user_role_ids = set(user_roles_qs.values_list('id', flat=True))
 | 
			
		||||
        if len(req_role_ids.intersection(user_role_ids)) == 0:
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
        is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        is_broker = False
 | 
			
		||||
        is_accountant = False
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    logger = logging.getLogger(__name__)
 | 
			
		||||
| 
						 | 
				
			
			@ -474,48 +540,31 @@ def add_quote_payment(request, instance_id, step_id):
 | 
			
		|||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    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)
 | 
			
		||||
    
 | 
			
		||||
    # Reset ALL subsequent completed steps to in_progress
 | 
			
		||||
    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()
 | 
			
		||||
        subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
        for subsequent_step in subsequent_steps:
 | 
			
		||||
            subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
            if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                # Bypass validation by using update() instead of save()
 | 
			
		||||
                instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                    status='in_progress',
 | 
			
		||||
                    completed_at=None
 | 
			
		||||
                )
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
 | 
			
		||||
                    pass
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    # On update, return to awaiting approval
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
 | 
			
		||||
| 
						 | 
				
			
			@ -525,22 +574,25 @@ def update_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, 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)
 | 
			
		||||
    # dynamic permission: users whose roles are among required approvers can delete payments
 | 
			
		||||
    
 | 
			
		||||
    # who can delete payments
 | 
			
		||||
    profile = getattr(request.user, 'profile', None)
 | 
			
		||||
    is_broker = False
 | 
			
		||||
    is_accountant = False
 | 
			
		||||
    try:
 | 
			
		||||
        req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
 | 
			
		||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
			
		||||
        user_role_ids = set(user_roles_qs.values_list('id', flat=True))
 | 
			
		||||
        if len(req_role_ids.intersection(user_role_ids)) == 0:
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
        is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
 | 
			
		||||
        is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
 | 
			
		||||
        is_broker = False
 | 
			
		||||
        is_accountant = False
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # soft delete using project's BaseModel delete override
 | 
			
		||||
| 
						 | 
				
			
			@ -556,46 +608,43 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # Reset ALL subsequent completed steps to in_progress
 | 
			
		||||
    try:
 | 
			
		||||
        subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
        for subsequent_step in subsequent_steps:
 | 
			
		||||
            subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
            if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                # Bypass validation by using update() instead of save()
 | 
			
		||||
                instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                    status='in_progress',
 | 
			
		||||
                    completed_at=None
 | 
			
		||||
                )
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    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})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def final_invoice_step(request, instance_id, step_id):
 | 
			
		||||
    """تجمیع اقلام پیشفاکتور با تغییرات نصب و صدور فاکتور نهایی"""
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
 | 
			
		||||
    instance = get_object_or_404(
 | 
			
		||||
        ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
 | 
			
		||||
        id=instance_id
 | 
			
		||||
| 
						 | 
				
			
			@ -734,7 +783,7 @@ def final_invoice_step(request, instance_id, step_id):
 | 
			
		|||
 | 
			
		||||
@login_required
 | 
			
		||||
def final_invoice_print(request, instance_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    items = invoice.items.select_related('item').filter(is_deleted=False).all()
 | 
			
		||||
    return render(request, 'invoices/final_invoice_print.html', {
 | 
			
		||||
| 
						 | 
				
			
			@ -747,7 +796,7 @@ def final_invoice_print(request, instance_id):
 | 
			
		|||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def approve_final_invoice(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can approve
 | 
			
		||||
| 
						 | 
				
			
			@ -756,14 +805,7 @@ def approve_final_invoice(request, instance_id, step_id):
 | 
			
		|||
            return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
			
		||||
    # Block approval when there is any remaining (positive or negative)
 | 
			
		||||
    invoice.calculate_totals()
 | 
			
		||||
    # if invoice.remaining_amount != 0:
 | 
			
		||||
    #     return JsonResponse({
 | 
			
		||||
    #         'success': False,
 | 
			
		||||
    #         'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})"
 | 
			
		||||
    #     })
 | 
			
		||||
    # mark step completed
 | 
			
		||||
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
    step_instance.status = 'completed'
 | 
			
		||||
    step_instance.completed_at = timezone.now()
 | 
			
		||||
| 
						 | 
				
			
			@ -782,7 +824,7 @@ def approve_final_invoice(request, instance_id, step_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def add_special_charge(request, instance_id, step_id):
 | 
			
		||||
    """افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی بهصورت آیتم جداگانه"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can add special charges
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -790,7 +832,7 @@ def add_special_charge(request, instance_id, step_id):
 | 
			
		|||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    # charge_type was removed from UI; we no longer require it
 | 
			
		||||
 | 
			
		||||
    item_id = request.POST.get('item_id')
 | 
			
		||||
    amount = (request.POST.get('amount') or '').strip()
 | 
			
		||||
    if not item_id:
 | 
			
		||||
| 
						 | 
				
			
			@ -805,7 +847,7 @@ def add_special_charge(request, instance_id, step_id):
 | 
			
		|||
    # Fetch existing special item from DB
 | 
			
		||||
    special_item = get_object_or_404(Item, id=item_id, is_special=True)
 | 
			
		||||
 | 
			
		||||
    from .models import InvoiceItem
 | 
			
		||||
    
 | 
			
		||||
    InvoiceItem.objects.create(
 | 
			
		||||
        invoice=invoice,
 | 
			
		||||
        item=special_item,
 | 
			
		||||
| 
						 | 
				
			
			@ -819,7 +861,7 @@ def add_special_charge(request, instance_id, step_id):
 | 
			
		|||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def delete_special_charge(request, instance_id, step_id, item_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # only MANAGER can delete special charges
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -827,7 +869,6 @@ def delete_special_charge(request, instance_id, step_id, item_id):
 | 
			
		|||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403)
 | 
			
		||||
    from .models import InvoiceItem
 | 
			
		||||
    inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice)
 | 
			
		||||
    # allow deletion only for special items
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -842,8 +883,9 @@ def delete_special_charge(request, instance_id, step_id, item_id):
 | 
			
		|||
 | 
			
		||||
@login_required
 | 
			
		||||
def final_settlement_step(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, 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')
 | 
			
		||||
| 
						 | 
				
			
			@ -854,6 +896,7 @@ def final_settlement_step(request, instance_id, step_id):
 | 
			
		|||
 | 
			
		||||
    # Ensure step instance exists
 | 
			
		||||
    step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
 | 
			
		||||
    
 | 
			
		||||
    # Build approver statuses for template
 | 
			
		||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
			
		||||
    approvals_map = {a.role_id: a.decision for a in step_instance.approvals.select_related('role').all()}
 | 
			
		||||
| 
						 | 
				
			
			@ -911,6 +954,13 @@ def final_settlement_step(request, instance_id, step_id):
 | 
			
		|||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
			
		||||
            )
 | 
			
		||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
			
		||||
            # If current step is ahead of this step, reset it back to this step (align behavior with other steps)
 | 
			
		||||
            try:
 | 
			
		||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
                    instance.current_step = step
 | 
			
		||||
                    instance.save(update_fields=['current_step'])
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
 | 
			
		||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -939,7 +989,7 @@ def final_settlement_step(request, instance_id, step_id):
 | 
			
		|||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def add_final_payment(request, instance_id, step_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # Only BROKER can add final settlement payments
 | 
			
		||||
| 
						 | 
				
			
			@ -948,6 +998,7 @@ def add_final_payment(request, instance_id, step_id):
 | 
			
		|||
            return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
| 
						 | 
				
			
			@ -1002,14 +1053,45 @@ def add_final_payment(request, instance_id, step_id):
 | 
			
		|||
    )
 | 
			
		||||
    # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX.
 | 
			
		||||
    invoice.refresh_from_db()
 | 
			
		||||
    # After payment change, set step back to in_progress
 | 
			
		||||
 | 
			
		||||
    # On delete, return to awaiting approval
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    # Reset ALL subsequent completed steps to in_progress
 | 
			
		||||
    try:
 | 
			
		||||
        subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
        for subsequent_step in subsequent_steps:
 | 
			
		||||
            subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
            if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                # Bypass validation by using update() instead of save()
 | 
			
		||||
                instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                    status='in_progress',
 | 
			
		||||
                    completed_at=None
 | 
			
		||||
                )
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return JsonResponse({
 | 
			
		||||
        'success': True,
 | 
			
		||||
        'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
 | 
			
		||||
| 
						 | 
				
			
			@ -1024,7 +1106,7 @@ def add_final_payment(request, instance_id, step_id):
 | 
			
		|||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
			
		||||
| 
						 | 
				
			
			@ -1036,44 +1118,47 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
			
		|||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
			
		||||
    payment.delete()
 | 
			
		||||
    invoice.refresh_from_db()
 | 
			
		||||
    # After payment change, set step back to in_progress
 | 
			
		||||
 | 
			
		||||
    # On delete, return to awaiting approval
 | 
			
		||||
    try:
 | 
			
		||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
			
		||||
        si.status = 'in_progress'
 | 
			
		||||
        si.completed_at = None
 | 
			
		||||
        si.save()
 | 
			
		||||
        si.approvals.all().delete()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # Reset ALL subsequent completed steps to in_progress
 | 
			
		||||
    try:
 | 
			
		||||
        subsequent_steps = instance.process.steps.filter(order__gt=step.order)
 | 
			
		||||
        for subsequent_step in subsequent_steps:
 | 
			
		||||
            subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
 | 
			
		||||
            if subsequent_step_instance and subsequent_step_instance.status == 'completed':
 | 
			
		||||
                # Bypass validation by using update() instead of save()
 | 
			
		||||
                instance.step_instances.filter(step=subsequent_step).update(
 | 
			
		||||
                    status='in_progress',
 | 
			
		||||
                    completed_at=None
 | 
			
		||||
                )
 | 
			
		||||
                # Clear previous approvals if the step requires re-approval
 | 
			
		||||
                try:
 | 
			
		||||
                    subsequent_step_instance.approvals.all().delete()
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # If current step is ahead of this step, reset it back to this step
 | 
			
		||||
    try:
 | 
			
		||||
        if instance.current_step and instance.current_step.order > step.order:
 | 
			
		||||
            instance.current_step = step
 | 
			
		||||
            instance.save(update_fields=['current_step'])
 | 
			
		||||
    except Exception:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
 | 
			
		||||
        'final_amount': str(invoice.final_amount),
 | 
			
		||||
        'paid_amount': str(invoice.paid_amount),
 | 
			
		||||
        'remaining_amount': str(invoice.remaining_amount),
 | 
			
		||||
    }})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def approve_final_settlement(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)
 | 
			
		||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
			
		||||
    # Block approval if any remaining exists (positive or negative)
 | 
			
		||||
    invoice.calculate_totals()
 | 
			
		||||
    if invoice.remaining_amount != 0:
 | 
			
		||||
        return JsonResponse({
 | 
			
		||||
            'success': False,
 | 
			
		||||
            'message': f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})"
 | 
			
		||||
        })
 | 
			
		||||
    # complete step
 | 
			
		||||
    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()
 | 
			
		||||
    # move next
 | 
			
		||||
    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])
 | 
			
		||||
    return JsonResponse({'success': True, 'message': 'تسویه حساب نهایی ثبت شد', 'redirect': redirect_url})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ class AffairsAdmin(admin.ModelAdmin):
 | 
			
		|||
@admin.register(Broker)
 | 
			
		||||
class BrokerAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['name', 'affairs', 'slug']
 | 
			
		||||
    list_filter = ['affairs__county__city', 'affairs__county', 'affairs']
 | 
			
		||||
    list_filter = ['affairs__county__city', 'affairs__county', 'affairs' ]
 | 
			
		||||
    search_fields = ['name', 'affairs__name', 'affairs__county__name']
 | 
			
		||||
    readonly_fields = ['deleted_at']
 | 
			
		||||
    prepopulated_fields = {'slug': ('name',)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								locations/migrations/0002_broker_company.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								locations/migrations/0002_broker_company.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 10:48
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0001_initial'),
 | 
			
		||||
        ('locations', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='broker',
 | 
			
		||||
            name='company',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.company', verbose_name='شرکت'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										17
									
								
								locations/migrations/0003_remove_broker_company.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								locations/migrations/0003_remove_broker_company.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 13:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('locations', '0002_broker_company'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='broker',
 | 
			
		||||
            name='company',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ class City(NameSlugModel):
 | 
			
		|||
        return self.name
 | 
			
		||||
 | 
			
		||||
class County(NameSlugModel):
 | 
			
		||||
    city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="شهرستان")
 | 
			
		||||
    city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="استان")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "شهرستان"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,10 +50,12 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		|||
    verbose_name_plural = "درخواستها"
 | 
			
		||||
    list_display = [
 | 
			
		||||
        'code',
 | 
			
		||||
        'current_step',
 | 
			
		||||
        'slug', 
 | 
			
		||||
        'well_display', 
 | 
			
		||||
        'representative', 
 | 
			
		||||
        'requester', 
 | 
			
		||||
        'broker', 
 | 
			
		||||
        'process', 
 | 
			
		||||
        'status_display', 
 | 
			
		||||
        'priority_display', 
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +67,8 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		|||
        'status', 
 | 
			
		||||
        'priority',
 | 
			
		||||
        'created',
 | 
			
		||||
        'well__representative'
 | 
			
		||||
        'well__representative',
 | 
			
		||||
        'broker'
 | 
			
		||||
    ]
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        'code',
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +89,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		|||
        'well', 
 | 
			
		||||
        'representative', 
 | 
			
		||||
        'requester', 
 | 
			
		||||
        'broker',
 | 
			
		||||
        'process', 
 | 
			
		||||
        'current_step'
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +103,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		|||
            'fields': ('well', 'representative')
 | 
			
		||||
        }),
 | 
			
		||||
        ('اطلاعات درخواست', {
 | 
			
		||||
            'fields': ('requester', 'priority')
 | 
			
		||||
            'fields': ('requester', 'broker', 'priority')
 | 
			
		||||
        }),
 | 
			
		||||
        ('وضعیت و پیشرفت', {
 | 
			
		||||
            'fields': ('status', 'current_step')
 | 
			
		||||
| 
						 | 
				
			
			@ -139,9 +143,9 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		|||
 | 
			
		||||
@admin.register(StepInstance)
 | 
			
		||||
class StepInstanceAdmin(SimpleHistoryAdmin):
 | 
			
		||||
    list_display = ['process_instance', 'step', 'assigned_to', 'status_display', 'rejection_count', 'started_at', 'completed_at']
 | 
			
		||||
    list_display = ['process_instance', 'process_instance__code', 'step', 'assigned_to', 'status_display', 'rejection_count', 'edit_count', 'started_at', 'completed_at']
 | 
			
		||||
    list_filter = ['status', 'step__process', 'started_at']
 | 
			
		||||
    search_fields = ['process_instance__name', 'step__name', 'assigned_to__username']
 | 
			
		||||
    search_fields = ['process_instance__name', 'process_instance__code', 'step__name', 'assigned_to__username']
 | 
			
		||||
    readonly_fields = ['started_at', 'completed_at']
 | 
			
		||||
    ordering = ['process_instance', 'step__order']
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								processes/migrations/0002_processinstance_broker.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								processes/migrations/0002_processinstance_broker.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-07 13:43
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('locations', '0003_remove_broker_company'),
 | 
			
		||||
        ('processes', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='processinstance',
 | 
			
		||||
            name='broker',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='process_instances', to='locations.broker', verbose_name='کارگزار'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
# Generated by Django 5.2.4 on 2025-09-08 08:18
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('processes', '0002_processinstance_broker'),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='historicalstepinstance',
 | 
			
		||||
            name='edit_count',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0, verbose_name='تعداد ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='historicalstepinstance',
 | 
			
		||||
            name='edited_after_completion',
 | 
			
		||||
            field=models.BooleanField(default=False, verbose_name='ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='historicalstepinstance',
 | 
			
		||||
            name='last_edited_at',
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True, verbose_name='آخرین زمان ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='historicalstepinstance',
 | 
			
		||||
            name='last_edited_by',
 | 
			
		||||
            field=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='ویرایش توسط'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='stepinstance',
 | 
			
		||||
            name='edit_count',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0, verbose_name='تعداد ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='stepinstance',
 | 
			
		||||
            name='edited_after_completion',
 | 
			
		||||
            field=models.BooleanField(default=False, verbose_name='ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='stepinstance',
 | 
			
		||||
            name='last_edited_at',
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True, verbose_name='آخرین زمان ویرایش پس از تکمیل'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='stepinstance',
 | 
			
		||||
            name='last_edited_by',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='step_instances_edited', to=settings.AUTH_USER_MODEL, verbose_name='ویرایش توسط'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ from simple_history.models import HistoricalRecords
 | 
			
		|||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from accounts.models import Role
 | 
			
		||||
from accounts.models import Role, Broker
 | 
			
		||||
from _helpers.utils import generate_unique_slug
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -189,6 +189,15 @@ class ProcessInstance(SluggedModel):
 | 
			
		|||
        verbose_name="اولویت"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    broker = models.ForeignKey(
 | 
			
		||||
        Broker,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name="کارگزار",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        related_name='process_instances'
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    completed_at = models.DateTimeField(
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
| 
						 | 
				
			
			@ -205,13 +214,6 @@ class ProcessInstance(SluggedModel):
 | 
			
		|||
            return f"{self.process.name} - {self.well.water_subscription_number}"
 | 
			
		||||
        return f"{self.process.name} - {self.requester.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        """اعتبارسنجی مدل"""
 | 
			
		||||
        if self.well and self.representative and self.well.representative != self.representative:
 | 
			
		||||
            raise ValidationError("نماینده درخواست باید همان نماینده ثبت شده در چاه باشد")
 | 
			
		||||
        
 | 
			
		||||
        if self.well and self.representative and self.requester == self.representative:
 | 
			
		||||
            raise ValidationError("درخواست کننده نمیتواند نماینده چاه باشد")
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        # Generate unique 5-digit numeric code if missing
 | 
			
		||||
| 
						 | 
				
			
			@ -233,6 +235,13 @@ class ProcessInstance(SluggedModel):
 | 
			
		|||
        if self.status == 'completed' and not self.completed_at:
 | 
			
		||||
            self.completed_at = timezone.now()
 | 
			
		||||
        
 | 
			
		||||
        # Auto-set broker if not already set
 | 
			
		||||
        if not self.broker:
 | 
			
		||||
            if self.well and hasattr(self.well, 'broker') and self.well.broker:
 | 
			
		||||
                self.broker = self.well.broker
 | 
			
		||||
            elif self.requester and hasattr(self.requester, 'profile') and self.requester.profile and hasattr(self.requester.profile, 'broker') and self.requester.profile.broker:
 | 
			
		||||
                self.broker = self.requester.profile.broker
 | 
			
		||||
        
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -318,6 +327,12 @@ class StepInstance(models.Model):
 | 
			
		|||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
 | 
			
		||||
    completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
 | 
			
		||||
    # Generic edit-tracking for post-completion modifications
 | 
			
		||||
    edited_after_completion = models.BooleanField(default=False, verbose_name="ویرایش پس از تکمیل")
 | 
			
		||||
    last_edited_at = models.DateTimeField(null=True, blank=True, verbose_name="آخرین زمان ویرایش پس از تکمیل")
 | 
			
		||||
    last_edited_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='step_instances_edited', verbose_name="ویرایش توسط")
 | 
			
		||||
    edit_count = models.PositiveIntegerField(default=0, verbose_name="تعداد ویرایش پس از تکمیل")
 | 
			
		||||
    
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										275
									
								
								processes/templates/processes/includes/instance_info_modal.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								processes/templates/processes/includes/instance_info_modal.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,275 @@
 | 
			
		|||
{% load common_tags %}
 | 
			
		||||
 | 
			
		||||
<!-- Modal for Instance Info -->
 | 
			
		||||
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-hidden="true">
 | 
			
		||||
  <div class="modal-dialog modal-lg">
 | 
			
		||||
    <div class="modal-content">
 | 
			
		||||
      <div class="modal-header">
 | 
			
		||||
        <h5 class="modal-title">اطلاعات درخواست {{ instance.code }}</h5>
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-body">
 | 
			
		||||
        <div class="row g-4">
 | 
			
		||||
          
 | 
			
		||||
          <!-- Well Information -->
 | 
			
		||||
          {% if well %}
 | 
			
		||||
          <div class="col-12">
 | 
			
		||||
            <div class="card border-0 bg-light">
 | 
			
		||||
              <div class="card-header bg-label-primary text-white py-2">
 | 
			
		||||
                <h6 class="mb-0">
 | 
			
		||||
                  <i class="bx bx-water me-2"></i>اطلاعات چاه
 | 
			
		||||
                </h6>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="card-body pt-3">
 | 
			
		||||
                <div class="row g-3">
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-droplet text-primary me-2"></i>
 | 
			
		||||
                      <strong>شماره اشتراک آب:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.water_subscription_number|default:"-" }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-bolt text-warning me-2"></i>
 | 
			
		||||
                      <strong>شماره اشتراک برق:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.electricity_subscription_number|default:"-" }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-barcode text-info me-2"></i>
 | 
			
		||||
                      <strong>سریال کنتور:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.water_meter_serial_number|default:"-" }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-barcode-reader text-secondary me-2"></i>
 | 
			
		||||
                      <strong>سریال قدیمی:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.water_meter_old_serial_number|default:"-" }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% if well.water_meter_manufacturer %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-factory text-success me-2"></i>
 | 
			
		||||
                      <strong>سازنده کنتور:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.water_meter_manufacturer.name }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-tachometer text-danger me-2"></i>
 | 
			
		||||
                      <strong>قدرت چاه:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.well_power|default:"-" }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% if well.utm_x and well.utm_y %}
 | 
			
		||||
                  <div class="col-12">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-map text-info me-2"></i>
 | 
			
		||||
                      <strong>مختصات:</strong>
 | 
			
		||||
                      <span class="ms-2">X: {{ well.utm_x }}, Y: {{ well.utm_y }}</span>
 | 
			
		||||
                      {% if well.utm_zone %}<span class="text-muted ms-2">(Zone: {{ well.utm_zone }})</span>{% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if well.county %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-map-pin text-warning me-2"></i>
 | 
			
		||||
                      <strong>شهرستان:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.county }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if well.affairs %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-building text-primary me-2"></i>
 | 
			
		||||
                      <strong>امور:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.affairs }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if well.reference_letter_number %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-file text-secondary me-2"></i>
 | 
			
		||||
                      <strong>شماره معرفی نامه:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ well.reference_letter_number }}</span>
 | 
			
		||||
                      {% if well.reference_letter_date %}
 | 
			
		||||
                        <span class="text-muted ms-2">({{ well.reference_letter_date|to_jalali }})</span>
 | 
			
		||||
                      {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if well.representative_letter_file %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-file text-secondary me-2"></i>
 | 
			
		||||
                      <strong>فایل نامه نمایندگی:</strong>
 | 
			
		||||
                      <a href="{{ well.representative_letter_file.url }}" class="ms-2">دانلود</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
 | 
			
		||||
          <!-- Representative Information -->
 | 
			
		||||
          {% if representative %}
 | 
			
		||||
          <div class="col-12">
 | 
			
		||||
            <div class="card border-0 bg-light">
 | 
			
		||||
              <div class="card-header bg-label-success text-white py-2">
 | 
			
		||||
                <h6 class="mb-0">
 | 
			
		||||
                  <i class="bx bx-user me-2"></i>اطلاعات نماینده
 | 
			
		||||
                </h6>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="card-body pt-3">
 | 
			
		||||
                <div class="row g-3">
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-user-circle text-primary me-2"></i>
 | 
			
		||||
                      <strong>نام و نام خانوادگی:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.get_full_name|default:representative.username }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% if representative.profile.national_code %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-id-card text-info me-2"></i>
 | 
			
		||||
                      <strong>کد ملی:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.national_code }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.phone_number_1 %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-phone text-success me-2"></i>
 | 
			
		||||
                      <strong>تلفن اول:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.phone_number_1 }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.phone_number_2 %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-phone text-success me-2"></i>
 | 
			
		||||
                      <strong>تلفن دوم:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.phone_number_2 }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.bank_name %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-credit-card text-warning me-2"></i>
 | 
			
		||||
                      <strong>بانک:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.get_bank_name_display }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.card_number %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-credit-card-alt text-secondary me-2"></i>
 | 
			
		||||
                      <strong>شماره کارت:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.card_number }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.account_number %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-wallet text-info me-2"></i>
 | 
			
		||||
                      <strong>شماره حساب:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ representative.profile.account_number }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if representative.profile.address %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-start mb-2">
 | 
			
		||||
                      <i class="bx bx-map text-danger me-2 mt-1"></i>
 | 
			
		||||
                      <div>
 | 
			
		||||
                        <strong>آدرس:</strong>
 | 
			
		||||
                        <p class="mb-0 ms-2 text-wrap">{{ representative.profile.address }}</p>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
 | 
			
		||||
          <!-- Process Information -->
 | 
			
		||||
          <div class="col-12">
 | 
			
		||||
            <div class="card border-0 bg-light">
 | 
			
		||||
              <div class="card-header bg-label-info text-white py-2">
 | 
			
		||||
                <h6 class="mb-0">
 | 
			
		||||
                  <i class="bx bx-cog me-2"></i>اطلاعات فرآیند
 | 
			
		||||
                </h6>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="card-body pt-3">
 | 
			
		||||
                <div class="row g-3">
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-list-ul text-primary me-2"></i>
 | 
			
		||||
                      <strong>نوع فرآیند:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ instance.process.name }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-calendar text-success me-2"></i>
 | 
			
		||||
                      <strong>تاریخ ایجاد:</strong>
 | 
			
		||||
                      <span class="ms-2">{{ instance.jcreated }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-check-circle text-info me-2"></i>
 | 
			
		||||
                      <strong>وضعیت:</strong>
 | 
			
		||||
                      <span class="ms-2 badge bg-label-primary">{{ instance.get_status_display }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% if instance.current_step %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-center mb-2">
 | 
			
		||||
                      <i class="bx bx-step-forward text-primary me-2"></i>
 | 
			
		||||
                      <strong>مرحله فعلی:</strong>
 | 
			
		||||
                      <span class="ms-2 badge bg-label-success">{{ instance.current_step.name }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                  {% if instance.description %}
 | 
			
		||||
                  <div class="col-md-6">
 | 
			
		||||
                    <div class="d-flex align-items-start mb-2">
 | 
			
		||||
                      <i class="bx bx-note text-secondary me-2 mt-1"></i>
 | 
			
		||||
                      <div>
 | 
			
		||||
                        <strong>توضیحات:</strong>
 | 
			
		||||
                        <p class="mb-0 ms-2 text-wrap">{{ instance.description }}</p>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">بستن</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
        {% extends '_base.html' %}
 | 
			
		||||
{% extends '_base.html' %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load humanize %}
 | 
			
		||||
{% load common_tags %}
 | 
			
		||||
{% load processes_tags %}
 | 
			
		||||
 | 
			
		||||
{% block sidebar %}
 | 
			
		||||
  {% include 'sidebars/admin.html' %}
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,11 @@
 | 
			
		|||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% include '_toasts.html' %}
 | 
			
		||||
 | 
			
		||||
<!-- Instance Info Modal -->
 | 
			
		||||
{% instance_info_modal instance %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div class="container-xxl flex-grow-1 container-p-y">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-12 mb-4">
 | 
			
		||||
| 
						 | 
				
			
			@ -22,16 +28,22 @@
 | 
			
		|||
        <div>
 | 
			
		||||
          <h4 class="mb-1">گزارش نهایی درخواست {{ instance.code }}</h4>
 | 
			
		||||
            <small class="text-muted d-block">
 | 
			
		||||
            اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
 | 
			
		||||
            | نماینده: {{ instance.representative.profile.national_code|default:"-" }}
 | 
			
		||||
            {% instance_info instance %}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="d-flex gap-2">
 | 
			
		||||
          {% if invoice %}
 | 
			
		||||
            <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت فاکتور</a>
 | 
			
		||||
            <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
			
		||||
              <i class="bx bx-printer me-2"></i> پرینت فاکتور
 | 
			
		||||
            </a>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          <a href="{% url 'certificates:certificate_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>
 | 
			
		||||
          <a href="{% url 'certificates:certificate_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-printer me-2"></i> پرینت گواهی
 | 
			
		||||
          </a>
 | 
			
		||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
			
		||||
            بازگشت
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
{% extends '_base.html' %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load accounts_tags %}
 | 
			
		||||
 | 
			
		||||
{% block sidebar %}
 | 
			
		||||
    {% include 'sidebars/admin.html' %}
 | 
			
		||||
| 
						 | 
				
			
			@ -43,10 +44,12 @@
 | 
			
		|||
              </span>
 | 
			
		||||
            </span>
 | 
			
		||||
          </button>
 | 
			
		||||
          {% if request.user|is_broker %}
 | 
			
		||||
          <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
 | 
			
		||||
            <i class="bx bx-plus me-1"></i>
 | 
			
		||||
            درخواست جدید
 | 
			
		||||
          </button>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +135,69 @@
 | 
			
		|||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  {% if access_denied %}
 | 
			
		||||
  <div class="alert alert-warning d-flex align-items-center mb-3" role="alert">
 | 
			
		||||
    <i class="bx bx-info-circle me-2"></i>
 | 
			
		||||
    <div>شما به این بخش دسترسی ندارید.</div>
 | 
			
		||||
  </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  <div class="card mb-3">
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
      <form method="get" class="row g-2 align-items-end">
 | 
			
		||||
        <div class="col-sm-6 col-md-3">
 | 
			
		||||
          <label class="form-label">وضعیت درخواست</label>
 | 
			
		||||
          <select class="form-select" name="status">
 | 
			
		||||
            <option value="">همه</option>
 | 
			
		||||
            {% for val, label in status_choices %}
 | 
			
		||||
              <option value="{{ val }}" {% if filter_status == val %}selected{% endif %}>{{ label }}</option>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if request.user|is_admin or request.user|is_manager or request.user|is_accountant %}
 | 
			
		||||
        <div class="col-sm-6 col-md-3">
 | 
			
		||||
          <label class="form-label">امور</label>
 | 
			
		||||
          <select class="form-select" name="affairs">
 | 
			
		||||
            <option value="">همه</option>
 | 
			
		||||
            {% for a in affairs_list %}
 | 
			
		||||
              <option value="{{ a.id }}" {% if filter_affairs|default:''|stringformat:'s' == a.id|stringformat:'s' %}selected{% endif %}>{{ a.name }}</option>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if request.user|is_admin or request.user|is_manager or request.user|is_accountant %}
 | 
			
		||||
        <div class="col-sm-6 col-md-3">
 | 
			
		||||
          <label class="form-label">کارگزار</label>
 | 
			
		||||
          <select class="form-select" name="broker">
 | 
			
		||||
            <option value="">همه</option>
 | 
			
		||||
            {% for b in brokers_list %}
 | 
			
		||||
              <option value="{{ b.id }}" {% if filter_broker|default:''|stringformat:'s' == b.id|stringformat:'s' %}selected{% endif %}>{{ b.name }}</option>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <div class="col-sm-6 col-md-3">
 | 
			
		||||
          <label class="form-label">مرحله فعلی</label>
 | 
			
		||||
          <select class="form-select" name="step">
 | 
			
		||||
            <option value="">همه</option>
 | 
			
		||||
            {% for s in steps_list %}
 | 
			
		||||
              <option value="{{ s.id }}" {% if filter_step|default:''|stringformat:'s' == s.id|stringformat:'s' %}selected{% endif %}>{{ s.process.name }} - {{ s.name }}</option>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-12 d-flex gap-2 justify-content-end mt-3">
 | 
			
		||||
          <button type="submit" class="btn btn-primary">
 | 
			
		||||
            <i class="bx bx-filter-alt me-1"></i>
 | 
			
		||||
            اعمال فیلتر
 | 
			
		||||
          </button>
 | 
			
		||||
          <a href="?" class="btn btn-outline-secondary">
 | 
			
		||||
            <i class="bx bx-x me-1"></i>
 | 
			
		||||
            حذف فیلتر
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="card">
 | 
			
		||||
    <div class="card-datatable table-responsive">
 | 
			
		||||
      <table id="requests-table" class="datatables-basic table border-top">
 | 
			
		||||
| 
						 | 
				
			
			@ -178,7 +244,7 @@
 | 
			
		|||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{{ item.instance.get_status_display_with_color|safe }}</td>
 | 
			
		||||
            <td>{{ item.instance.jcreated }}</td>
 | 
			
		||||
            <td>{{ item.instance.jcreated_date }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="d-inline-block">
 | 
			
		||||
                <a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
 | 
			
		||||
| 
						 | 
				
			
			@ -196,19 +262,31 @@
 | 
			
		|||
                      </a>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                  </li>
 | 
			
		||||
                  {% if request.user|is_broker %}
 | 
			
		||||
                  <div class="dropdown-divider"></div>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <a href="#" class="dropdown-item text-danger" data-instance-id="{{ item.instance.id }}" data-instance-code="{{ item.instance.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
 | 
			
		||||
                      <i class="bx bx-trash me-1"></i>حذف
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </ul>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% empty %}
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td colspan="11" class="text-center text-muted">موردی ثبت نشده است</td>
 | 
			
		||||
            <td class="text-center text-muted">موردی ثبت نشده است</td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
| 
						 | 
				
			
			@ -479,7 +557,7 @@
 | 
			
		|||
    $('#requests-table').DataTable({
 | 
			
		||||
      pageLength: 10,
 | 
			
		||||
      lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
 | 
			
		||||
      order: [[0, 'desc']],
 | 
			
		||||
      order: [],
 | 
			
		||||
      responsive: true,
 | 
			
		||||
    });
 | 
			
		||||
    let currentWellId = null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
from django import template
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from ..models import ProcessInstance, StepInstance
 | 
			
		||||
from ..utils import count_incomplete_instances
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,3 +51,62 @@ def stepper_header(instance, current_step=None):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
# moved to _base/common/templatetags/common_tags.py
 | 
			
		||||
 | 
			
		||||
@register.inclusion_tag('processes/includes/instance_info_modal.html')
 | 
			
		||||
def instance_info_modal(instance, modal_id=None):
 | 
			
		||||
    """
 | 
			
		||||
    نمایش مدال اطلاعات کامل چاه و نماینده
 | 
			
		||||
    
 | 
			
		||||
    استفاده:
 | 
			
		||||
    {% load processes_tags %}
 | 
			
		||||
    {% instance_info_modal instance %}
 | 
			
		||||
    
 | 
			
		||||
    یا با modal_id سفارشی:
 | 
			
		||||
    {% instance_info_modal instance "myCustomModal" %}
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(instance, ProcessInstance):
 | 
			
		||||
        return {}
 | 
			
		||||
    
 | 
			
		||||
    if not modal_id:
 | 
			
		||||
        modal_id = f"instanceInfoModal_{instance.id}"
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
        'instance': instance,
 | 
			
		||||
        'modal_id': modal_id,
 | 
			
		||||
        'well': instance.well,
 | 
			
		||||
        'representative': instance.representative,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def instance_info(instance, modal_id=None):
 | 
			
		||||
    """
 | 
			
		||||
    آیکون info برای نمایش مدال اطلاعات
 | 
			
		||||
    
 | 
			
		||||
    استفاده:
 | 
			
		||||
    {% load processes_tags %}
 | 
			
		||||
    نام کاربر: {{ user.name }} {% instance_info_icon instance %}
 | 
			
		||||
    
 | 
			
		||||
    یا با modal_id سفارشی:
 | 
			
		||||
    {% instance_info_icon instance "myCustomModal" %}
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(instance, ProcessInstance):
 | 
			
		||||
        return ""
 | 
			
		||||
    
 | 
			
		||||
    if not modal_id:
 | 
			
		||||
        modal_id = f"instanceInfoModal_{instance.id}"
 | 
			
		||||
    
 | 
			
		||||
    html = f'''
 | 
			
		||||
    اشتراک آب: {instance.well.water_subscription_number }
 | 
			
		||||
    | نماینده: {instance.representative.profile.national_code }
 | 
			
		||||
    <i class="bx bx-info-circle text-muted ms-1" 
 | 
			
		||||
       style="cursor: pointer; font-size: 14px;" 
 | 
			
		||||
       data-bs-toggle="modal" 
 | 
			
		||||
       data-bs-target="#{modal_id}" 
 | 
			
		||||
       title="اطلاعات کامل چاه و نماینده"></i>
 | 
			
		||||
    '''
 | 
			
		||||
    return mark_safe(html)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def incomplete_requests_count(user):
 | 
			
		||||
    return count_incomplete_instances(user)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										118
									
								
								processes/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								processes/utils.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from .models import ProcessInstance
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def scope_instances_queryset(user, queryset=None):
 | 
			
		||||
    """Return a queryset of ProcessInstance scoped by the user's role.
 | 
			
		||||
 | 
			
		||||
    If no profile/role, returns an empty queryset.
 | 
			
		||||
    """
 | 
			
		||||
    qs = queryset if queryset is not None else ProcessInstance.objects.all()
 | 
			
		||||
    profile = getattr(user, 'profile', None)
 | 
			
		||||
    if not profile:
 | 
			
		||||
        return qs.none()
 | 
			
		||||
    try:
 | 
			
		||||
        if profile.has_role(UserRoles.INSTALLER):
 | 
			
		||||
            # Only instances assigned to this installer
 | 
			
		||||
            from installations.models import InstallationAssignment
 | 
			
		||||
            assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
 | 
			
		||||
            return qs.filter(id__in=assign_ids)
 | 
			
		||||
        if profile.has_role(UserRoles.BROKER):
 | 
			
		||||
            return qs.filter(broker=profile.broker)
 | 
			
		||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
			
		||||
            return qs.filter(broker__affairs__county=profile.county)
 | 
			
		||||
        if profile.has_role(UserRoles.ADMIN):
 | 
			
		||||
            return qs
 | 
			
		||||
        # if profile.has_role(UserRoles.WATER_RESOURCE_MANAGER) or profile.has_role(UserRoles.HEADQUARTER):
 | 
			
		||||
        #     return qs.filter(well__county=profile.county)
 | 
			
		||||
        # Fallback: no special scope
 | 
			
		||||
        # return qs
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return qs.none()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def count_incomplete_instances(user):
 | 
			
		||||
    """Count non-completed, non-deleted requests within the user's scope."""
 | 
			
		||||
    base = ProcessInstance.objects.select_related('well').filter(is_deleted=False).exclude(status='completed')
 | 
			
		||||
    return scope_instances_queryset(user, base).count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def user_can_access_instance(user, instance: ProcessInstance) -> bool:
 | 
			
		||||
    """Check if user can access a specific instance based on scoping rules."""
 | 
			
		||||
    try:
 | 
			
		||||
        scoped = scope_instances_queryset(user, ProcessInstance.objects.filter(id=instance.id))
 | 
			
		||||
        return scoped.exists()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_scoped_instance_or_404(request, instance_id: int) -> ProcessInstance:
 | 
			
		||||
    """Return instance only if it's within the user's scope; otherwise 404.
 | 
			
		||||
 | 
			
		||||
    Use this in any view receiving instance_id from URL to prevent URL tampering.
 | 
			
		||||
    """
 | 
			
		||||
    base = ProcessInstance.objects.filter(is_deleted=False)
 | 
			
		||||
    qs = scope_instances_queryset(request.user, base)
 | 
			
		||||
    return get_object_or_404(qs, id=instance_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def scope_wells_queryset(user, queryset=None):
 | 
			
		||||
    """Return a queryset of Well scoped by the user's role (parity with instances)."""
 | 
			
		||||
    try:
 | 
			
		||||
        from wells.models import Well
 | 
			
		||||
        qs = queryset if queryset is not None else Well.objects.all()
 | 
			
		||||
        profile = getattr(user, 'profile', None)
 | 
			
		||||
        if not profile:
 | 
			
		||||
            return qs.none()
 | 
			
		||||
        if profile.has_role(UserRoles.ADMIN):
 | 
			
		||||
            return qs
 | 
			
		||||
        if profile.has_role(UserRoles.BROKER):
 | 
			
		||||
            return qs.filter(broker=profile.broker)
 | 
			
		||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
			
		||||
            return qs.filter(broker__affairs__county=profile.county)
 | 
			
		||||
        if profile.has_role(UserRoles.INSTALLER):
 | 
			
		||||
            # Wells that have instances assigned to this installer
 | 
			
		||||
            from installations.models import InstallationAssignment
 | 
			
		||||
            assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
 | 
			
		||||
            inst_qs = ProcessInstance.objects.filter(id__in=assign_ids)
 | 
			
		||||
            return qs.filter(process_instances__in=inst_qs).distinct()
 | 
			
		||||
        # Fallback
 | 
			
		||||
        return qs.none()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return qs.none() if 'qs' in locals() else []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def scope_customers_queryset(user, queryset=None):
 | 
			
		||||
    """Return a queryset of customer Profiles scoped by user's role.
 | 
			
		||||
 | 
			
		||||
    Assumes queryset is Profiles already filtered to customers, otherwise we filter here.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        from accounts.models import Profile
 | 
			
		||||
        qs = queryset if queryset is not None else Profile.objects.all()
 | 
			
		||||
        # Ensure we're only looking at customer profiles
 | 
			
		||||
        from common.consts import UserRoles as UR
 | 
			
		||||
        qs = qs.filter(roles__slug=UR.CUSTOMER.value, is_deleted=False)
 | 
			
		||||
 | 
			
		||||
        profile = getattr(user, 'profile', None)
 | 
			
		||||
        if not profile:
 | 
			
		||||
            return qs.none()
 | 
			
		||||
        if profile.has_role(UserRoles.ADMIN):
 | 
			
		||||
            return qs
 | 
			
		||||
        if profile.has_role(UserRoles.BROKER):
 | 
			
		||||
            return qs.filter(broker=profile.broker)
 | 
			
		||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
			
		||||
            return qs.filter(county=profile.county)
 | 
			
		||||
        if profile.has_role(UserRoles.INSTALLER):
 | 
			
		||||
            # Customers that are representatives of instances assigned to this installer
 | 
			
		||||
            from installations.models import InstallationAssignment
 | 
			
		||||
            assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
 | 
			
		||||
            rep_ids = ProcessInstance.objects.filter(id__in=assign_ids).values_list('representative', flat=True)
 | 
			
		||||
            return qs.filter(user_id__in=rep_ids)
 | 
			
		||||
        # Fallback
 | 
			
		||||
        return qs.none()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return qs.none() if 'qs' in locals() else []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -7,19 +7,62 @@ from django.http import JsonResponse
 | 
			
		|||
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, ProcessStep
 | 
			
		||||
from .utils import scope_instances_queryset, get_scoped_instance_or_404
 | 
			
		||||
from installations.models import InstallationAssignment
 | 
			
		||||
from wells.models import Well
 | 
			
		||||
from accounts.models import Profile
 | 
			
		||||
from accounts.models import Profile, Broker
 | 
			
		||||
from locations.models import Affairs
 | 
			
		||||
from accounts.forms import CustomerForm
 | 
			
		||||
from wells.forms import WellForm
 | 
			
		||||
from wells.models import WaterMeterManufacturer
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def request_list(request):
 | 
			
		||||
    """نمایش لیست درخواستها با جدول و مدال ایجاد"""
 | 
			
		||||
    instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created')
 | 
			
		||||
    instances = ProcessInstance.objects.select_related('well', 'representative', 'requester', 'broker', 'current_step', 'process').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created')
 | 
			
		||||
    access_denied = False
 | 
			
		||||
 | 
			
		||||
    # filter by roles (scoped queryset)
 | 
			
		||||
    try:
 | 
			
		||||
        instances = scope_instances_queryset(request.user, instances)
 | 
			
		||||
        if not instances.exists() and not getattr(request.user, 'profile', None):
 | 
			
		||||
            access_denied = True
 | 
			
		||||
            instances = instances.none()
 | 
			
		||||
    except Exception:
 | 
			
		||||
        access_denied = True
 | 
			
		||||
        instances = instances.none()
 | 
			
		||||
        
 | 
			
		||||
    # Filters
 | 
			
		||||
    status_q = (request.GET.get('status') or '').strip()
 | 
			
		||||
    affairs_q = (request.GET.get('affairs') or '').strip()
 | 
			
		||||
    broker_q = (request.GET.get('broker') or '').strip()
 | 
			
		||||
    step_q = (request.GET.get('step') or '').strip()
 | 
			
		||||
 | 
			
		||||
    if status_q:
 | 
			
		||||
        instances = instances.filter(status=status_q)
 | 
			
		||||
    if affairs_q:
 | 
			
		||||
        try:
 | 
			
		||||
            instances = instances.filter(well__affairs_id=int(affairs_q))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
    if broker_q:
 | 
			
		||||
        try:
 | 
			
		||||
            instances = instances.filter(broker_id=int(broker_q))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
    if step_q:
 | 
			
		||||
        try:
 | 
			
		||||
            instances = instances.filter(current_step_id=int(step_q))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
    processes = Process.objects.filter(is_active=True)
 | 
			
		||||
    status_choices = list(ProcessInstance.STATUS_CHOICES)
 | 
			
		||||
    affairs_list = Affairs.objects.all().order_by('name')
 | 
			
		||||
    brokers_list = Broker.objects.all().order_by('name')
 | 
			
		||||
    steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order')
 | 
			
		||||
    manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
 | 
			
		||||
    
 | 
			
		||||
    # Calculate progress for each instance
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +95,16 @@ def request_list(request):
 | 
			
		|||
        'completed_count': completed_count,
 | 
			
		||||
        'in_progress_count': in_progress_count,
 | 
			
		||||
        'pending_count': pending_count,
 | 
			
		||||
        # filter context
 | 
			
		||||
        'status_choices': status_choices,
 | 
			
		||||
        'affairs_list': affairs_list,
 | 
			
		||||
        'brokers_list': brokers_list,
 | 
			
		||||
        'steps_list': steps_list,
 | 
			
		||||
        'filter_status': status_q,
 | 
			
		||||
        'filter_affairs': affairs_q,
 | 
			
		||||
        'filter_broker': broker_q,
 | 
			
		||||
        'filter_step': step_q,
 | 
			
		||||
        'access_denied': access_denied,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -125,6 +178,13 @@ def lookup_representative_by_national_code(request):
 | 
			
		|||
def create_request_with_entities(request):
 | 
			
		||||
    """ایجاد/بهروزرسانی چاه و نماینده و سپس ایجاد درخواست"""
 | 
			
		||||
    User = get_user_model()
 | 
			
		||||
    # Only BROKER can create requests
 | 
			
		||||
    try:
 | 
			
		||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403)
 | 
			
		||||
        
 | 
			
		||||
    process_id = request.POST.get('process')
 | 
			
		||||
    process = Process.objects.get(id=process_id)
 | 
			
		||||
    description = request.POST.get('description', '')
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +290,14 @@ def create_request_with_entities(request):
 | 
			
		|||
            well.broker = current_profile.broker
 | 
			
		||||
        well.save()
 | 
			
		||||
 | 
			
		||||
    # Ensure no active (non-deleted, non-completed) request exists for this well
 | 
			
		||||
    try:
 | 
			
		||||
        active_exists = ProcessInstance.objects.filter(well=well, is_deleted=False).exclude(status='completed').exists()
 | 
			
		||||
        if active_exists:
 | 
			
		||||
            return JsonResponse({'ok': False, 'error': 'برای این چاه یک درخواست جاری وجود دارد. ابتدا آن را تکمیل یا حذف کنید.'}, status=400)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'ok': False, 'error': 'خطا در بررسی وضعیت درخواستهای قبلی این چاه'}, status=400)
 | 
			
		||||
 | 
			
		||||
    # Create request instance
 | 
			
		||||
    instance = ProcessInstance.objects.create(
 | 
			
		||||
        process=process,
 | 
			
		||||
| 
						 | 
				
			
			@ -237,6 +305,7 @@ def create_request_with_entities(request):
 | 
			
		|||
        well=well,
 | 
			
		||||
        representative=representative_user,
 | 
			
		||||
        requester=request.user,
 | 
			
		||||
        broker=request.user.profile.broker if request.user.profile else None,
 | 
			
		||||
        status='pending',
 | 
			
		||||
        priority='medium',
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +329,17 @@ def create_request_with_entities(request):
 | 
			
		|||
@login_required
 | 
			
		||||
def delete_request(request, instance_id):
 | 
			
		||||
    """حذف درخواست"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    # Only BROKER can delete requests and only within their scope
 | 
			
		||||
    try:
 | 
			
		||||
        profile = getattr(request.user, 'profile', None)
 | 
			
		||||
        if not (profile and profile.has_role(UserRoles.BROKER)):
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
 | 
			
		||||
        # Enforce ownership by broker (prevent deleting others' requests)
 | 
			
		||||
        if instance.broker_id and profile.broker and instance.broker_id != profile.broker.id:
 | 
			
		||||
            return JsonResponse({'success': False, 'message': 'شما مجاز به حذف این درخواست نیستید'}, status=403)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
 | 
			
		||||
    code = instance.code
 | 
			
		||||
    if instance.status == 'completed':
 | 
			
		||||
        return JsonResponse({
 | 
			
		||||
| 
						 | 
				
			
			@ -277,10 +356,10 @@ def delete_request(request, instance_id):
 | 
			
		|||
@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
 | 
			
		||||
    )
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    # Prefetch for performance
 | 
			
		||||
    instance = ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile').get(id=instance.id)
 | 
			
		||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
			
		||||
    # If the request is already completed, redirect to read-only summary page
 | 
			
		||||
    if instance.status == 'completed':
 | 
			
		||||
| 
						 | 
				
			
			@ -338,7 +417,8 @@ def step_detail(request, instance_id, step_id):
 | 
			
		|||
@login_required 
 | 
			
		||||
def instance_steps(request, instance_id):
 | 
			
		||||
    """هدایت به مرحله فعلی instance"""
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance, id=instance_id)
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
    
 | 
			
		||||
    if not instance.current_step:
 | 
			
		||||
        # اگر مرحله فعلی تعریف نشده، به اولین مرحله برو
 | 
			
		||||
| 
						 | 
				
			
			@ -360,6 +440,9 @@ def instance_steps(request, instance_id):
 | 
			
		|||
@login_required
 | 
			
		||||
def instance_summary(request, instance_id):
 | 
			
		||||
    """نمای خلاصهٔ فقطخواندنی برای درخواستهای تکمیلشده."""
 | 
			
		||||
    # Enforce scoped access to prevent URL tampering
 | 
			
		||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
			
		||||
 | 
			
		||||
    instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id)
 | 
			
		||||
    # Only show for completed requests; otherwise route to steps
 | 
			
		||||
    if instance.status != 'completed':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
{% load static %}
 | 
			
		||||
{% load accounts_tags %}
 | 
			
		||||
<!-- Menu -->
 | 
			
		||||
 | 
			
		||||
<aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme">
 | 
			
		||||
| 
						 | 
				
			
			@ -108,9 +109,12 @@
 | 
			
		|||
      <a href="{% url 'processes:request_list' %}" class="menu-link">
 | 
			
		||||
        <i class="menu-icon tf-icons bx bx-user"></i>
 | 
			
		||||
        <div class="text-truncate">درخواستها</div>
 | 
			
		||||
        {% load processes_tags %}
 | 
			
		||||
        <span class="badge badge-center rounded-pill bg-danger ms-auto">{% incomplete_requests_count request.user %}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
  {% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant %}
 | 
			
		||||
    <!-- Customers -->
 | 
			
		||||
    <li class="menu-header small text-uppercase">
 | 
			
		||||
      <span class="menu-header-text">مشترکها</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -131,11 +135,11 @@
 | 
			
		|||
        <div class="text-truncate">چاهها</div>
 | 
			
		||||
      </a>
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <!-- Apps & Pages -->
 | 
			
		||||
    <li class="menu-header small text-uppercase">
 | 
			
		||||
    <li class="menu-header small text-uppercase d-none">
 | 
			
		||||
      <span class="menu-header-text">گزارشها</span>
 | 
			
		||||
    </li>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,12 +82,10 @@ class WellForm(forms.ModelForm):
 | 
			
		|||
            'utm_x': forms.NumberInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': 'X UTM',
 | 
			
		||||
                'step': '0.000001'
 | 
			
		||||
            }),
 | 
			
		||||
            'utm_y': forms.NumberInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
                'placeholder': 'Y UTM',
 | 
			
		||||
                'step': '0.000001'
 | 
			
		||||
            }),
 | 
			
		||||
            'utm_zone': forms.NumberInput(attrs={
 | 
			
		||||
                'class': 'form-control',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,14 +78,14 @@ class Well(SluggedModel):
 | 
			
		|||
 | 
			
		||||
    utm_x = models.DecimalField(
 | 
			
		||||
        max_digits=10,
 | 
			
		||||
        decimal_places=6,
 | 
			
		||||
        decimal_places=0,
 | 
			
		||||
        verbose_name="X UTM",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True
 | 
			
		||||
        )
 | 
			
		||||
    utm_y = models.DecimalField(
 | 
			
		||||
        max_digits=10,
 | 
			
		||||
        decimal_places=6,
 | 
			
		||||
        decimal_places=0,
 | 
			
		||||
        verbose_name="Y UTM",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -163,12 +163,19 @@
 | 
			
		|||
          </tr>
 | 
			
		||||
          {% empty %}
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td colspan="7" class="text-center py-4">
 | 
			
		||||
            <td class="text-center py-4">
 | 
			
		||||
              <div class="text-muted">
 | 
			
		||||
                <i class="ti ti-database-off ti-lg mb-2"></i>
 | 
			
		||||
                <p>چاهی یافت نشد</p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
            <td></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,17 +7,23 @@ from django.contrib import messages
 | 
			
		|||
from django import forms
 | 
			
		||||
from .models import Well, WaterMeterManufacturer
 | 
			
		||||
from .forms import WellForm, WaterMeterManufacturerForm
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from common.decorators import allowed_roles
 | 
			
		||||
from common.consts import UserRoles
 | 
			
		||||
from processes.utils import scope_wells_queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def well_list(request):
 | 
			
		||||
    """نمایش لیست چاهها"""
 | 
			
		||||
    wells = Well.objects.select_related(
 | 
			
		||||
    base = Well.objects.select_related(
 | 
			
		||||
        'representative', 
 | 
			
		||||
        'water_meter_manufacturer',
 | 
			
		||||
        'affairs',
 | 
			
		||||
        'county', 
 | 
			
		||||
        'broker'
 | 
			
		||||
    ).filter(is_deleted=False)
 | 
			
		||||
    wells = scope_wells_queryset(request.user, base)
 | 
			
		||||
    
 | 
			
		||||
    # فرم برای افزودن چاه جدید
 | 
			
		||||
    form = WellForm()
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +37,8 @@ def well_list(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def add_well_ajax(request):
 | 
			
		||||
    """AJAX endpoint for adding wells"""
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +95,8 @@ def add_well_ajax(request):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def edit_well_ajax(request, well_id):
 | 
			
		||||
    """AJAX endpoint for editing wells"""
 | 
			
		||||
    well = get_object_or_404(Well, id=well_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -141,6 +151,8 @@ def edit_well_ajax(request, well_id):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
			
		||||
def delete_well(request, well_id):
 | 
			
		||||
    """حذف چاه"""
 | 
			
		||||
    well = get_object_or_404(Well, id=well_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -154,6 +166,7 @@ def delete_well(request, well_id):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_GET
 | 
			
		||||
@login_required
 | 
			
		||||
def get_well_data(request, well_id):
 | 
			
		||||
    """دریافت اطلاعات چاه برای ویرایش"""
 | 
			
		||||
    well = get_object_or_404(Well, id=well_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -183,6 +196,7 @@ def get_well_data(request, well_id):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@require_POST
 | 
			
		||||
@login_required
 | 
			
		||||
def create_water_meter_manufacturer(request):
 | 
			
		||||
    """ایجاد شرکت سازنده کنتور آب جدید"""
 | 
			
		||||
    form = WaterMeterManufacturerForm(request.POST)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue