Merge remote-tracking branch 'origin' into shafafiyat/production
This commit is contained in:
		
						commit
						494b7743e9
					
				
					 51 changed files with 2397 additions and 326 deletions
				
			
		| 
						 | 
					@ -173,3 +173,6 @@ JAZZMIN_SETTINGS = {
 | 
				
			||||||
    "custom_js": None,
 | 
					    "custom_js": None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# VAT / Value Added Tax percent (e.g., 0.09 for 9%)
 | 
				
			||||||
 | 
					VAT_RATE = 0.1
 | 
				
			||||||
| 
						 | 
					@ -144,7 +144,7 @@ def persian_converter2(time):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def persian_converter3(time):
 | 
					def persian_converter3(time):
 | 
				
			||||||
    time = time + datetime.timedelta(days=1)
 | 
					    time = time
 | 
				
			||||||
    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
					    time_to_str = "{},{},{}".format(time.year, time.month, time.day)
 | 
				
			||||||
    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
					    time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
 | 
				
			||||||
    time_to_list = list(time_to_tuple)
 | 
					    time_to_list = list(time_to_tuple)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,8 @@ class ProfileAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_display = [
 | 
					    list_display = [
 | 
				
			||||||
        "user",
 | 
					        "user",
 | 
				
			||||||
        "fullname",
 | 
					        "fullname",
 | 
				
			||||||
 | 
					        "user_type_display",
 | 
				
			||||||
 | 
					        "company_name",
 | 
				
			||||||
        "pic_tag",
 | 
					        "pic_tag",
 | 
				
			||||||
        "roles_str",
 | 
					        "roles_str",
 | 
				
			||||||
        "affairs",
 | 
					        "affairs",
 | 
				
			||||||
| 
						 | 
					@ -25,8 +27,52 @@ class ProfileAdmin(admin.ModelAdmin):
 | 
				
			||||||
        "is_active",
 | 
					        "is_active",
 | 
				
			||||||
        "jcreated",
 | 
					        "jcreated",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number']
 | 
					    search_fields = [
 | 
				
			||||||
    list_filter = ['user', 'roles', 'affairs', 'county', 'broker']
 | 
					        'user__username', 
 | 
				
			||||||
 | 
					        'user__first_name', 
 | 
				
			||||||
 | 
					        'user__last_name', 
 | 
				
			||||||
 | 
					        'user__phone_number',
 | 
				
			||||||
 | 
					        'company_name',
 | 
				
			||||||
 | 
					        'company_national_id',
 | 
				
			||||||
 | 
					        'national_code'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    list_filter = [
 | 
				
			||||||
 | 
					        'user_type',
 | 
				
			||||||
 | 
					        'user', 
 | 
				
			||||||
 | 
					        'roles', 
 | 
				
			||||||
 | 
					        'affairs', 
 | 
				
			||||||
 | 
					        'county', 
 | 
				
			||||||
 | 
					        'broker',
 | 
				
			||||||
 | 
					        'is_completed',
 | 
				
			||||||
 | 
					        'is_active'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    fieldsets = (
 | 
				
			||||||
 | 
					        ('اطلاعات کاربری', {
 | 
				
			||||||
 | 
					            'fields': ('user', 'user_type', 'pic', 'roles')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات شخصی - حقیقی', {
 | 
				
			||||||
 | 
					            'fields': ('national_code', 'address', 'phone_number_1', 'phone_number_2'),
 | 
				
			||||||
 | 
					            'classes': ('collapse',),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات شرکت - حقوقی', {
 | 
				
			||||||
 | 
					            'fields': ('company_name', 'company_national_id'),
 | 
				
			||||||
 | 
					            'classes': ('collapse',),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات بانکی', {
 | 
				
			||||||
 | 
					            'fields': ('card_number', 'account_number', 'bank_name'),
 | 
				
			||||||
 | 
					            'classes': ('collapse',),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('اطلاعات سازمانی', {
 | 
				
			||||||
 | 
					            'fields': ('affairs', 'county', 'broker', 'owner'),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('وضعیت', {
 | 
				
			||||||
 | 
					            'fields': ('is_completed', 'is_active'),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('تاریخها', {
 | 
				
			||||||
 | 
					            'fields': ('created', 'updated'),
 | 
				
			||||||
 | 
					            'classes': ('collapse',),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    date_hierarchy = 'created'
 | 
					    date_hierarchy = 'created'
 | 
				
			||||||
    ordering = ['-created']
 | 
					    ordering = ['-created']
 | 
				
			||||||
    readonly_fields = ['created', 'updated']
 | 
					    readonly_fields = ['created', 'updated']
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ from django import forms
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from django.contrib.auth.forms import UserCreationForm
 | 
					from django.contrib.auth.forms import UserCreationForm
 | 
				
			||||||
from .models import Profile, Role
 | 
					from .models import Profile, Role
 | 
				
			||||||
from common.consts import UserRoles
 | 
					from common.consts import UserRoles, USER_TYPE_CHOICES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
User = get_user_model()
 | 
					User = get_user_model()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,10 +28,15 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Profile
 | 
					        model = Profile
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            'phone_number_1', 'phone_number_2', 'national_code', 
 | 
					            'user_type', 'phone_number_1', 'phone_number_2', 'national_code', 
 | 
				
			||||||
 | 
					            'company_name', 'company_national_id',
 | 
				
			||||||
            'address', 'card_number', 'account_number', 'bank_name'
 | 
					            'address', 'card_number', 'account_number', 'bank_name'
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            'user_type': forms.Select(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'id': 'user-type-select'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
            'phone_number_1': forms.TextInput(attrs={
 | 
					            'phone_number_1': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': '09123456789'
 | 
					                'placeholder': '09123456789'
 | 
				
			||||||
| 
						 | 
					@ -46,6 +51,15 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
                'maxlength': '10',
 | 
					                'maxlength': '10',
 | 
				
			||||||
                'required': 'required'
 | 
					                'required': 'required'
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
 | 
					            'company_name': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control company-field',
 | 
				
			||||||
 | 
					                'placeholder': 'نام شرکت'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'company_national_id': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control company-field',
 | 
				
			||||||
 | 
					                'placeholder': 'شناسه ملی شرکت',
 | 
				
			||||||
 | 
					                'maxlength': '11'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
            'address': forms.Textarea(attrs={
 | 
					            'address': forms.Textarea(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': 'آدرس کامل',
 | 
					                'placeholder': 'آدرس کامل',
 | 
				
			||||||
| 
						 | 
					@ -67,9 +81,12 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        labels = {
 | 
					        labels = {
 | 
				
			||||||
 | 
					            'user_type': 'نوع کاربر',
 | 
				
			||||||
            'phone_number_1': 'تلفن ۱',
 | 
					            'phone_number_1': 'تلفن ۱',
 | 
				
			||||||
            'phone_number_2': 'تلفن ۲',
 | 
					            'phone_number_2': 'تلفن ۲',
 | 
				
			||||||
            'national_code': 'کد ملی',
 | 
					            'national_code': 'کد ملی',
 | 
				
			||||||
 | 
					            'company_name': 'نام شرکت',
 | 
				
			||||||
 | 
					            'company_national_id': 'شناسه ملی شرکت',
 | 
				
			||||||
            'address': 'آدرس',
 | 
					            'address': 'آدرس',
 | 
				
			||||||
            'card_number': 'شماره کارت',
 | 
					            'card_number': 'شماره کارت',
 | 
				
			||||||
            'account_number': 'شماره حساب',
 | 
					            'account_number': 'شماره حساب',
 | 
				
			||||||
| 
						 | 
					@ -89,6 +106,21 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
                raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.')
 | 
					                raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.')
 | 
				
			||||||
        return national_code
 | 
					        return national_code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean(self):
 | 
				
			||||||
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
 | 
					        user_type = cleaned_data.get('user_type')
 | 
				
			||||||
 | 
					        company_name = cleaned_data.get('company_name')
 | 
				
			||||||
 | 
					        company_national_id = cleaned_data.get('company_national_id')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # If user type is legal, company fields are required
 | 
				
			||||||
 | 
					        if user_type == 'legal':
 | 
				
			||||||
 | 
					            if not company_name:
 | 
				
			||||||
 | 
					                self.add_error('company_name', 'برای کاربران حقوقی نام شرکت الزامی است.')
 | 
				
			||||||
 | 
					            if not company_national_id:
 | 
				
			||||||
 | 
					                self.add_error('company_national_id', 'برای کاربران حقوقی شناسه ملی شرکت الزامی است.')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return cleaned_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, commit=True):
 | 
					    def save(self, commit=True):
 | 
				
			||||||
        def _compute_completed(cleaned):
 | 
					        def _compute_completed(cleaned):
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
| 
						 | 
					@ -100,7 +132,15 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
                bank_ok = bool(cleaned.get('bank_name'))
 | 
					                bank_ok = bool(cleaned.get('bank_name'))
 | 
				
			||||||
                card_ok = bool((cleaned.get('card_number') or '').strip())
 | 
					                card_ok = bool((cleaned.get('card_number') or '').strip())
 | 
				
			||||||
                acc_ok = bool((cleaned.get('account_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])
 | 
					                
 | 
				
			||||||
 | 
					                # Check user type specific requirements
 | 
				
			||||||
 | 
					                user_type = cleaned.get('user_type', 'individual')
 | 
				
			||||||
 | 
					                if user_type == 'legal':
 | 
				
			||||||
 | 
					                    company_name_ok = bool((cleaned.get('company_name') or '').strip())
 | 
				
			||||||
 | 
					                    company_id_ok = bool((cleaned.get('company_national_id') or '').strip())
 | 
				
			||||||
 | 
					                    return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok, company_name_ok, company_id_ok])
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok])
 | 
				
			||||||
            except Exception:
 | 
					            except Exception:
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
        # Check if this is an update (instance exists)
 | 
					        # Check if this is an update (instance exists)
 | 
				
			||||||
| 
						 | 
					@ -125,9 +165,12 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
            profile.is_completed = _compute_completed({
 | 
					            profile.is_completed = _compute_completed({
 | 
				
			||||||
                'first_name': user.first_name,
 | 
					                'first_name': user.first_name,
 | 
				
			||||||
                'last_name': user.last_name,
 | 
					                'last_name': user.last_name,
 | 
				
			||||||
 | 
					                'user_type': self.cleaned_data.get('user_type'),
 | 
				
			||||||
                'national_code': self.cleaned_data.get('national_code'),
 | 
					                'national_code': self.cleaned_data.get('national_code'),
 | 
				
			||||||
                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
					                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
				
			||||||
                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
					                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
				
			||||||
 | 
					                'company_name': self.cleaned_data.get('company_name'),
 | 
				
			||||||
 | 
					                'company_national_id': self.cleaned_data.get('company_national_id'),
 | 
				
			||||||
                'address': self.cleaned_data.get('address'),
 | 
					                'address': self.cleaned_data.get('address'),
 | 
				
			||||||
                'bank_name': self.cleaned_data.get('bank_name'),
 | 
					                'bank_name': self.cleaned_data.get('bank_name'),
 | 
				
			||||||
                'card_number': self.cleaned_data.get('card_number'),
 | 
					                'card_number': self.cleaned_data.get('card_number'),
 | 
				
			||||||
| 
						 | 
					@ -171,9 +214,12 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
            profile.is_completed = _compute_completed({
 | 
					            profile.is_completed = _compute_completed({
 | 
				
			||||||
                'first_name': user.first_name,
 | 
					                'first_name': user.first_name,
 | 
				
			||||||
                'last_name': user.last_name,
 | 
					                'last_name': user.last_name,
 | 
				
			||||||
 | 
					                'user_type': self.cleaned_data.get('user_type'),
 | 
				
			||||||
                'national_code': self.cleaned_data.get('national_code'),
 | 
					                'national_code': self.cleaned_data.get('national_code'),
 | 
				
			||||||
                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
					                'phone_number_1': self.cleaned_data.get('phone_number_1'),
 | 
				
			||||||
                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
					                'phone_number_2': self.cleaned_data.get('phone_number_2'),
 | 
				
			||||||
 | 
					                'company_name': self.cleaned_data.get('company_name'),
 | 
				
			||||||
 | 
					                'company_national_id': self.cleaned_data.get('company_national_id'),
 | 
				
			||||||
                'address': self.cleaned_data.get('address'),
 | 
					                'address': self.cleaned_data.get('address'),
 | 
				
			||||||
                'bank_name': self.cleaned_data.get('bank_name'),
 | 
					                'bank_name': self.cleaned_data.get('bank_name'),
 | 
				
			||||||
                'card_number': self.cleaned_data.get('card_number'),
 | 
					                'card_number': self.cleaned_data.get('card_number'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-21 07:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0006_company_card_holder_name'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalprofile',
 | 
				
			||||||
 | 
					            name='company_name',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=255, null=True, verbose_name='نام شرکت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalprofile',
 | 
				
			||||||
 | 
					            name='company_national_id',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=11, null=True, validators=[django.core.validators.RegexValidator(code='invalid_company_national_id', message='شناسه ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شناسه ملی شرکت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalprofile',
 | 
				
			||||||
 | 
					            name='user_type',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('individual', 'حقیقی'), ('legal', 'حقوقی')], default='individual', max_length=20, verbose_name='نوع کاربر'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='profile',
 | 
				
			||||||
 | 
					            name='company_name',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=255, null=True, verbose_name='نام شرکت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='profile',
 | 
				
			||||||
 | 
					            name='company_national_id',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=11, null=True, validators=[django.core.validators.RegexValidator(code='invalid_company_national_id', message='شناسه ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شناسه ملی شرکت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='profile',
 | 
				
			||||||
 | 
					            name='user_type',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('individual', 'حقیقی'), ('legal', 'حقوقی')], default='individual', max_length=20, verbose_name='نوع کاربر'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ from django.utils.html import format_html
 | 
				
			||||||
from django.core.validators import RegexValidator
 | 
					from django.core.validators import RegexValidator
 | 
				
			||||||
from simple_history.models import HistoricalRecords
 | 
					from simple_history.models import HistoricalRecords
 | 
				
			||||||
from common.models import TagModel, BaseModel, NameSlugModel
 | 
					from common.models import TagModel, BaseModel, NameSlugModel
 | 
				
			||||||
from common.consts import UserRoles, BANK_CHOICES
 | 
					from common.consts import UserRoles, BANK_CHOICES, USER_TYPE_CHOICES
 | 
				
			||||||
from locations.models import Affairs, Broker, County
 | 
					from locations.models import Affairs, Broker, County
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,6 +88,33 @@ class Profile(BaseModel):
 | 
				
			||||||
        verbose_name="شماره تماس ۲",
 | 
					        verbose_name="شماره تماس ۲",
 | 
				
			||||||
        blank=True
 | 
					        blank=True
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    user_type = models.CharField(
 | 
				
			||||||
 | 
					        max_length=20,
 | 
				
			||||||
 | 
					        choices=USER_TYPE_CHOICES,
 | 
				
			||||||
 | 
					        default='individual',
 | 
				
			||||||
 | 
					        verbose_name="نوع کاربر"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    company_national_id = models.CharField(
 | 
				
			||||||
 | 
					        max_length=11,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        verbose_name="شناسه ملی شرکت",
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            RegexValidator(
 | 
				
			||||||
 | 
					                regex=r'^\d+$',
 | 
				
			||||||
 | 
					                message='شناسه ملی باید فقط شامل اعداد باشد.',
 | 
				
			||||||
 | 
					                code='invalid_company_national_id'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        help_text="فقط برای کاربران حقوقی الزامی است"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    company_name = models.CharField(
 | 
				
			||||||
 | 
					        max_length=255,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        verbose_name="نام شرکت",
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        help_text="فقط برای کاربران حقوقی الزامی است"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pic = models.ImageField(
 | 
					    pic = models.ImageField(
 | 
				
			||||||
        upload_to="profile_images",
 | 
					        upload_to="profile_images",
 | 
				
			||||||
| 
						 | 
					@ -179,6 +206,23 @@ class Profile(BaseModel):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pic_tag.short_description = "تصویر"
 | 
					    pic_tag.short_description = "تصویر"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_legal_entity(self):
 | 
				
			||||||
 | 
					        return self.user_type == 'legal'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_individual(self):
 | 
				
			||||||
 | 
					        return self.user_type == 'individual'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_display_name(self):
 | 
				
			||||||
 | 
					        """Returns appropriate display name based on user type"""
 | 
				
			||||||
 | 
					        if self.is_legal_entity() and self.company_name:
 | 
				
			||||||
 | 
					            return self.company_name
 | 
				
			||||||
 | 
					        return self.user.get_full_name() or str(self.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def user_type_display(self):
 | 
				
			||||||
 | 
					        return dict(USER_TYPE_CHOICES).get(self.user_type, self.user_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_type_display.short_description = "نوع کاربر"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Company(NameSlugModel):
 | 
					class Company(NameSlugModel):
 | 
				
			||||||
    logo = models.ImageField(
 | 
					    logo = models.ImageField(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,6 +61,7 @@
 | 
				
			||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
            <th>ردیف</th>
 | 
					            <th>ردیف</th>
 | 
				
			||||||
            <th>کاربر</th>
 | 
					            <th>کاربر</th>
 | 
				
			||||||
 | 
					            <th>نوع کاربر</th>
 | 
				
			||||||
            <th>کد ملی</th>
 | 
					            <th>کد ملی</th>
 | 
				
			||||||
            <th>تلفن</th>
 | 
					            <th>تلفن</th>
 | 
				
			||||||
            <th>آدرس</th>
 | 
					            <th>آدرس</th>
 | 
				
			||||||
| 
						 | 
					@ -100,6 +101,27 @@
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					              {% if customer.user_type == 'legal' %}
 | 
				
			||||||
 | 
					                <span class="badge bg-label-info">
 | 
				
			||||||
 | 
					                  <i class="bx bx-buildings me-1"></i>حقوقی
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <div class="mt-1">
 | 
				
			||||||
 | 
					                  {% if customer.company_name %}
 | 
				
			||||||
 | 
					                    <small class="text-muted d-block">{{ customer.company_name|truncatechars:25 }}</small>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if customer.company_national_id %}
 | 
				
			||||||
 | 
					                    <small class="text-muted d-block">
 | 
				
			||||||
 | 
					                      <i class="bx bx-id-card me-1"></i>{{ customer.company_national_id }}
 | 
				
			||||||
 | 
					                    </small>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              {% else %}
 | 
				
			||||||
 | 
					                <span class="badge bg-label-primary">
 | 
				
			||||||
 | 
					                  <i class="bx bx-user me-1"></i>حقیقی
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              {% endif %}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
            <td>{{ customer.national_code|default:"کد ملی ثبت نشده" }}</td>
 | 
					            <td>{{ customer.national_code|default:"کد ملی ثبت نشده" }}</td>
 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
              <div class="d-flex flex-column">
 | 
					              <div class="d-flex flex-column">
 | 
				
			||||||
| 
						 | 
					@ -205,6 +227,16 @@
 | 
				
			||||||
      <input type="hidden" id="customer-id" name="customer_id" value="">
 | 
					      <input type="hidden" id="customer-id" name="customer_id" value="">
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      <!-- User Information -->
 | 
					      <!-- User Information -->
 | 
				
			||||||
 | 
					      <div class="col-sm-12">
 | 
				
			||||||
 | 
					        <label class="form-label fw-bold" for="{{ form.user_type.id_for_label }}">{{ form.user_type.label }}</label>
 | 
				
			||||||
 | 
					        <div class="input-group input-group-merge">
 | 
				
			||||||
 | 
					          <span class="input-group-text"><i class="bx bx-user-circle"></i></span>
 | 
				
			||||||
 | 
					          {{ form.user_type }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {% if form.user_type.errors %}
 | 
				
			||||||
 | 
					          <div class="invalid-feedback d-block">{{ form.user_type.errors.0 }}</div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
     
 | 
					     
 | 
				
			||||||
      <div class="col-sm-6">
 | 
					      <div class="col-sm-6">
 | 
				
			||||||
        <label class="form-label fw-bold" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
 | 
					        <label class="form-label fw-bold" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
 | 
				
			||||||
| 
						 | 
					@ -261,6 +293,29 @@
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- Company Information (for legal entities) -->
 | 
				
			||||||
 | 
					      <div class="col-sm-12 company-fields" style="display: none;">
 | 
				
			||||||
 | 
					        <label class="form-label fw-bold" for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label>
 | 
				
			||||||
 | 
					        <div class="input-group input-group-merge">
 | 
				
			||||||
 | 
					          <span class="input-group-text"><i class="bx bx-buildings"></i></span>
 | 
				
			||||||
 | 
					          {{ form.company_name }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {% if form.company_name.errors %}
 | 
				
			||||||
 | 
					          <div class="invalid-feedback d-block">{{ form.company_name.errors.0 }}</div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="col-sm-12 company-fields" style="display: none;">
 | 
				
			||||||
 | 
					        <label class="form-label fw-bold" for="{{ form.company_national_id.id_for_label }}">{{ form.company_national_id.label }}</label>
 | 
				
			||||||
 | 
					        <div class="input-group input-group-merge">
 | 
				
			||||||
 | 
					          <span class="input-group-text"><i class="bx bx-id-card"></i></span>
 | 
				
			||||||
 | 
					          {{ form.company_national_id }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {% if form.company_national_id.errors %}
 | 
				
			||||||
 | 
					          <div class="invalid-feedback d-block">{{ form.company_national_id.errors.0 }}</div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="col-sm-12">
 | 
					      <div class="col-sm-12">
 | 
				
			||||||
        <label class="form-label fw-bold" for="{{ form.bank_name.id_for_label }}">{{ form.bank_name.label }}</label>
 | 
					        <label class="form-label fw-bold" for="{{ form.bank_name.id_for_label }}">{{ form.bank_name.label }}</label>
 | 
				
			||||||
        <div class="input-group input-group-merge">
 | 
					        <div class="input-group input-group-merge">
 | 
				
			||||||
| 
						 | 
					@ -347,6 +402,18 @@
 | 
				
			||||||
                        <td class="text-muted"><i class="bx bx-fingerprint me-1"></i>کد ملی</td>
 | 
					                        <td class="text-muted"><i class="bx bx-fingerprint me-1"></i>کد ملی</td>
 | 
				
			||||||
                        <td><strong id="cd-national-code">-</strong></td>
 | 
					                        <td><strong id="cd-national-code">-</strong></td>
 | 
				
			||||||
                      </tr>
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                      <tr>
 | 
				
			||||||
 | 
					                        <td class="text-muted"><i class="bx bx-user-circle me-1"></i>نوع کاربر</td>
 | 
				
			||||||
 | 
					                        <td><strong id="cd-user-type">-</strong></td>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                      <tr id="cd-company-name-row" style="display: none;">
 | 
				
			||||||
 | 
					                        <td class="text-muted"><i class="bx bx-buildings me-1"></i>نام شرکت</td>
 | 
				
			||||||
 | 
					                        <td><strong id="cd-company-name">-</strong></td>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
 | 
					                      <tr id="cd-company-id-row" style="display: none;">
 | 
				
			||||||
 | 
					                        <td class="text-muted"><i class="bx bx-id-card me-1"></i>شناسه ملی شرکت</td>
 | 
				
			||||||
 | 
					                        <td><strong id="cd-company-id">-</strong></td>
 | 
				
			||||||
 | 
					                      </tr>
 | 
				
			||||||
                      <tr>
 | 
					                      <tr>
 | 
				
			||||||
                        <td class="text-muted"><i class="bx bx-phone me-1"></i>شماره تلفن اول</td>
 | 
					                        <td class="text-muted"><i class="bx bx-phone me-1"></i>شماره تلفن اول</td>
 | 
				
			||||||
                        <td><strong id="cd-phone1">-</strong></td>
 | 
					                        <td><strong id="cd-phone1">-</strong></td>
 | 
				
			||||||
| 
						 | 
					@ -495,6 +562,9 @@
 | 
				
			||||||
      lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
 | 
					      lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
 | 
				
			||||||
      order: [[0, 'asc']],
 | 
					      order: [[0, 'asc']],
 | 
				
			||||||
      responsive: true,
 | 
					      responsive: true,
 | 
				
			||||||
 | 
					      columnDefs: [
 | 
				
			||||||
 | 
					        { targets: [8], orderable: false } // عملیات column غیرقابل مرتبسازی
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Handle form submission
 | 
					    // Handle form submission
 | 
				
			||||||
| 
						 | 
					@ -603,6 +673,21 @@
 | 
				
			||||||
        $('#cd-username').text(c.user.username || '-');
 | 
					        $('#cd-username').text(c.user.username || '-');
 | 
				
			||||||
        $('#cd-fullname').text(c.user.full_name || '-');
 | 
					        $('#cd-fullname').text(c.user.full_name || '-');
 | 
				
			||||||
        $('#cd-national-code').text(c.national_code || '-');
 | 
					        $('#cd-national-code').text(c.national_code || '-');
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // User type and company information
 | 
				
			||||||
 | 
					        const userTypeDisplay = c.user_type === 'legal' ? 'حقوقی' : 'حقیقی';
 | 
				
			||||||
 | 
					        $('#cd-user-type').text(userTypeDisplay);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (c.user_type === 'legal') {
 | 
				
			||||||
 | 
					          $('#cd-company-name').text(c.company_name || '-');
 | 
				
			||||||
 | 
					          $('#cd-company-id').text(c.company_national_id || '-');
 | 
				
			||||||
 | 
					          $('#cd-company-name-row').show();
 | 
				
			||||||
 | 
					          $('#cd-company-id-row').show();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          $('#cd-company-name-row').hide();
 | 
				
			||||||
 | 
					          $('#cd-company-id-row').hide();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        $('#cd-phone1').text(c.phone_number_1 || '-');
 | 
					        $('#cd-phone1').text(c.phone_number_1 || '-');
 | 
				
			||||||
        $('#cd-phone2').text(c.phone_number_2 || '-');
 | 
					        $('#cd-phone2').text(c.phone_number_2 || '-');
 | 
				
			||||||
        $('#cd-email').text(c.user.email || '-');
 | 
					        $('#cd-email').text(c.user.email || '-');
 | 
				
			||||||
| 
						 | 
					@ -689,9 +774,12 @@
 | 
				
			||||||
            'customer-id': customer.id,
 | 
					            'customer-id': customer.id,
 | 
				
			||||||
            'id_first_name': customer.first_name,
 | 
					            'id_first_name': customer.first_name,
 | 
				
			||||||
            'id_last_name': customer.last_name,
 | 
					            'id_last_name': customer.last_name,
 | 
				
			||||||
 | 
					            'user-type-select': customer.user_type,
 | 
				
			||||||
            'id_phone_number_1': customer.phone_number_1,
 | 
					            'id_phone_number_1': customer.phone_number_1,
 | 
				
			||||||
            'id_phone_number_2': customer.phone_number_2,
 | 
					            'id_phone_number_2': customer.phone_number_2,
 | 
				
			||||||
            'id_national_code': customer.national_code,
 | 
					            'id_national_code': customer.national_code,
 | 
				
			||||||
 | 
					            'id_company_name': customer.company_name,
 | 
				
			||||||
 | 
					            'id_company_national_id': customer.company_national_id,
 | 
				
			||||||
            'id_card_number': customer.card_number,
 | 
					            'id_card_number': customer.card_number,
 | 
				
			||||||
            'id_account_number': customer.account_number,
 | 
					            'id_account_number': customer.account_number,
 | 
				
			||||||
            'id_address': customer.address,
 | 
					            'id_address': customer.address,
 | 
				
			||||||
| 
						 | 
					@ -711,6 +799,14 @@
 | 
				
			||||||
          if (customer.bank_name !== undefined && customer.bank_name !== null) {
 | 
					          if (customer.bank_name !== undefined && customer.bank_name !== null) {
 | 
				
			||||||
            $('#id_bank_name').val(customer.bank_name);
 | 
					            $('#id_bank_name').val(customer.bank_name);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Ensure user type is applied and toggle company fields
 | 
				
			||||||
 | 
					          if (customer.user_type !== undefined && customer.user_type !== null) {
 | 
				
			||||||
 | 
					            $('#user-type-select').val(customer.user_type);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Toggle company fields based on user type
 | 
				
			||||||
 | 
					          toggleCompanyFields();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Open modal
 | 
					          // Open modal
 | 
				
			||||||
          $('#add-new-record').offcanvas('show');
 | 
					          $('#add-new-record').offcanvas('show');
 | 
				
			||||||
| 
						 | 
					@ -753,8 +849,39 @@
 | 
				
			||||||
    $('.is-invalid').removeClass('is-invalid');
 | 
					    $('.is-invalid').removeClass('is-invalid');
 | 
				
			||||||
    $('.invalid-feedback').remove();
 | 
					    $('.invalid-feedback').remove();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Reset user type to individual and hide company fields
 | 
				
			||||||
 | 
					    $('#user-type-select').val('individual');
 | 
				
			||||||
 | 
					    toggleCompanyFields();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Open modal
 | 
					    // Open modal
 | 
				
			||||||
    $('#add-new-record').offcanvas('show');
 | 
					    $('#add-new-record').offcanvas('show');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  function toggleCompanyFields() {
 | 
				
			||||||
 | 
					    const userType = $('#user-type-select').val();
 | 
				
			||||||
 | 
					    const companyFields = $('.company-fields');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (userType === 'legal') {
 | 
				
			||||||
 | 
					      companyFields.show();
 | 
				
			||||||
 | 
					      // Make company fields required
 | 
				
			||||||
 | 
					      $('input[name="company_name"]').attr('required', true);
 | 
				
			||||||
 | 
					      $('input[name="company_national_id"]').attr('required', true);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      companyFields.hide();
 | 
				
			||||||
 | 
					      // Remove required attribute from company fields
 | 
				
			||||||
 | 
					      $('input[name="company_name"]').removeAttr('required').val('');
 | 
				
			||||||
 | 
					      $('input[name="company_national_id"]').removeAttr('required').val('');
 | 
				
			||||||
 | 
					      // Clear any validation errors for company fields
 | 
				
			||||||
 | 
					      $('.company-fields .is-invalid').removeClass('is-invalid');
 | 
				
			||||||
 | 
					      $('.company-fields .invalid-feedback').remove();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Initialize user type toggle functionality
 | 
				
			||||||
 | 
					  $(document).ready(function() {
 | 
				
			||||||
 | 
					    $('#user-type-select').on('change', toggleCompanyFields);
 | 
				
			||||||
 | 
					    // Initialize on page load
 | 
				
			||||||
 | 
					    toggleCompanyFields();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ def dashboard(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def customer_list(request):
 | 
					def customer_list(request):
 | 
				
			||||||
    # Get all profiles that have customer role
 | 
					    # Get all profiles that have customer role
 | 
				
			||||||
    base = 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')
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ def customer_list(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_POST
 | 
					@require_POST
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def add_customer_ajax(request):
 | 
					def add_customer_ajax(request):
 | 
				
			||||||
    """AJAX endpoint for adding customers"""
 | 
					    """AJAX endpoint for adding customers"""
 | 
				
			||||||
    form = CustomerForm(request.POST, request.FILES)
 | 
					    form = CustomerForm(request.POST, request.FILES)
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,7 @@ def add_customer_ajax(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_POST
 | 
					@require_POST
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def edit_customer_ajax(request, customer_id):
 | 
					def edit_customer_ajax(request, customer_id):
 | 
				
			||||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
					    customer = get_object_or_404(Profile, id=customer_id)
 | 
				
			||||||
    form = CustomerForm(request.POST, request.FILES, instance=customer)
 | 
					    form = CustomerForm(request.POST, request.FILES, instance=customer)
 | 
				
			||||||
| 
						 | 
					@ -148,9 +148,12 @@ def get_customer_data(request, customer_id):
 | 
				
			||||||
    form_html = {
 | 
					    form_html = {
 | 
				
			||||||
        'first_name': str(form['first_name']),
 | 
					        'first_name': str(form['first_name']),
 | 
				
			||||||
        'last_name': str(form['last_name']),
 | 
					        'last_name': str(form['last_name']),
 | 
				
			||||||
 | 
					        'user_type': str(form['user_type']),
 | 
				
			||||||
        'phone_number_1': str(form['phone_number_1']),
 | 
					        'phone_number_1': str(form['phone_number_1']),
 | 
				
			||||||
        'phone_number_2': str(form['phone_number_2']),
 | 
					        'phone_number_2': str(form['phone_number_2']),
 | 
				
			||||||
        'national_code': str(form['national_code']),
 | 
					        'national_code': str(form['national_code']),
 | 
				
			||||||
 | 
					        'company_name': str(form['company_name']),
 | 
				
			||||||
 | 
					        'company_national_id': str(form['company_national_id']),
 | 
				
			||||||
        'card_number': str(form['card_number']),
 | 
					        'card_number': str(form['card_number']),
 | 
				
			||||||
        'account_number': str(form['account_number']),
 | 
					        'account_number': str(form['account_number']),
 | 
				
			||||||
        'address': str(form['address']),
 | 
					        'address': str(form['address']),
 | 
				
			||||||
| 
						 | 
					@ -163,9 +166,12 @@ def get_customer_data(request, customer_id):
 | 
				
			||||||
            'id': customer.id,
 | 
					            'id': customer.id,
 | 
				
			||||||
            'first_name': customer.user.first_name,
 | 
					            'first_name': customer.user.first_name,
 | 
				
			||||||
            'last_name': customer.user.last_name,
 | 
					            'last_name': customer.user.last_name,
 | 
				
			||||||
 | 
					            'user_type': customer.user_type or 'individual',
 | 
				
			||||||
            'phone_number_1': customer.phone_number_1 or '',
 | 
					            'phone_number_1': customer.phone_number_1 or '',
 | 
				
			||||||
            'phone_number_2': customer.phone_number_2 or '',
 | 
					            'phone_number_2': customer.phone_number_2 or '',
 | 
				
			||||||
            'national_code': customer.national_code or '',
 | 
					            'national_code': customer.national_code or '',
 | 
				
			||||||
 | 
					            'company_name': customer.company_name or '',
 | 
				
			||||||
 | 
					            'company_national_id': customer.company_national_id or '',
 | 
				
			||||||
            'card_number': customer.card_number or '',
 | 
					            'card_number': customer.card_number or '',
 | 
				
			||||||
            'account_number': customer.account_number or '',
 | 
					            'account_number': customer.account_number or '',
 | 
				
			||||||
            'address': customer.address or '',
 | 
					            'address': customer.address or '',
 | 
				
			||||||
| 
						 | 
					@ -177,7 +183,7 @@ def get_customer_data(request, customer_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def get_customer_details(request, customer_id):
 | 
					def get_customer_details(request, customer_id):
 | 
				
			||||||
    """جزئیات کامل مشترک برای نمایش در مدال"""
 | 
					    """جزئیات کامل مشترک برای نمایش در مدال"""
 | 
				
			||||||
    customer = get_object_or_404(
 | 
					    customer = get_object_or_404(
 | 
				
			||||||
| 
						 | 
					@ -196,6 +202,9 @@ def get_customer_details(request, customer_id):
 | 
				
			||||||
            'date_joined': customer.jcreated_date() if customer.user.date_joined else '',
 | 
					            'date_joined': customer.jcreated_date() if customer.user.date_joined else '',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        'national_code': customer.national_code or '',
 | 
					        'national_code': customer.national_code or '',
 | 
				
			||||||
 | 
					        'user_type': customer.user_type or 'individual',
 | 
				
			||||||
 | 
					        'company_name': customer.company_name or '',
 | 
				
			||||||
 | 
					        'company_national_id': customer.company_national_id or '',
 | 
				
			||||||
        'phone_number_1': customer.phone_number_1 or '',
 | 
					        'phone_number_1': customer.phone_number_1 or '',
 | 
				
			||||||
        'phone_number_2': customer.phone_number_2 or '',
 | 
					        'phone_number_2': customer.phone_number_2 or '',
 | 
				
			||||||
        'card_number': customer.card_number or '',
 | 
					        'card_number': customer.card_number or '',
 | 
				
			||||||
| 
						 | 
					@ -229,7 +238,7 @@ def get_customer_details(request, customer_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def get_customer_wells(request, customer_id):
 | 
					def get_customer_wells(request, customer_id):
 | 
				
			||||||
    """چاههای مرتبط با یک مشترک"""
 | 
					    """چاههای مرتبط با یک مشترک"""
 | 
				
			||||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
					    customer = get_object_or_404(Profile, id=customer_id)
 | 
				
			||||||
| 
						 | 
					@ -262,7 +271,7 @@ def get_customer_wells(request, customer_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def get_customer_requests(request, customer_id):
 | 
					def get_customer_requests(request, customer_id):
 | 
				
			||||||
    """درخواستهای مرتبط با یک مشترک"""
 | 
					    """درخواستهای مرتبط با یک مشترک"""
 | 
				
			||||||
    customer = get_object_or_404(Profile, id=customer_id)
 | 
					    customer = get_object_or_404(Profile, id=customer_id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-27 15:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('certificates', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='certificateinstance',
 | 
				
			||||||
 | 
					            name='hologram_code',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='کد یکتا هولوگرام'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ class CertificateInstance(BaseModel):
 | 
				
			||||||
    issued_at = models.DateField(auto_now_add=True, verbose_name='تاریخ صدور')
 | 
					    issued_at = models.DateField(auto_now_add=True, verbose_name='تاریخ صدور')
 | 
				
			||||||
    approved = models.BooleanField(default=False, verbose_name='تایید شده')
 | 
					    approved = models.BooleanField(default=False, verbose_name='تایید شده')
 | 
				
			||||||
    approved_at = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تایید')
 | 
					    approved_at = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تایید')
 | 
				
			||||||
 | 
					    hologram_code = models.CharField(max_length=50, null=True, blank=True, verbose_name='کد یکتا هولوگرام')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = 'گواهی'
 | 
					        verbose_name = 'گواهی'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,19 +18,20 @@
 | 
				
			||||||
  <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
					  <link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <style>
 | 
					  <style>
 | 
				
			||||||
    @page { size: A4; margin: 1cm; }
 | 
					    @page { size: A4 landscape; margin: 1cm; }
 | 
				
			||||||
    @media print { body { print-color-adjust: exact; } .no-print { display: none !important; } }
 | 
					    @media print { body { print-color-adjust: exact; } .no-print { display: none !important; } }
 | 
				
			||||||
    .header { border-bottom: 1px solid #dee2e6; padding-bottom: 16px; margin-bottom: 24px; }
 | 
					    .header { border-bottom: 0px solid #dee2e6; padding-bottom: 10px; margin-bottom: 10px; }
 | 
				
			||||||
    .company-name { font-weight: 600; }
 | 
					    .company-name { font-weight: 600; }
 | 
				
			||||||
    .body-text { white-space: pre-line; line-height: 1.9; }
 | 
					    .body-text { white-space: pre-line; line-height: 1.9; }
 | 
				
			||||||
    .signature-section { margin-top: 40px; border-top: 1px solid #dee2e6; padding-top: 24px; }
 | 
					    .signature-section { margin-top: 40px; border-top: 0px solid #dee2e6; padding-top: 24px; }
 | 
				
			||||||
  </style>
 | 
					  </style>
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
  <div class="container-fluid py-3">
 | 
					  <div class="container-fluid py-3">
 | 
				
			||||||
    <!-- Top-left request info -->
 | 
					    <!-- Top-left request info -->
 | 
				
			||||||
    <div class="d-flex mb-2">
 | 
					    <div class="d-flex">
 | 
				
			||||||
      <div class="ms-auto text-end">
 | 
					      <div class="ms-auto text-end">
 | 
				
			||||||
 | 
					        <div class="">کد یکتا هولوگرام: {{ cert.hologram_code|default:'-' }}</div>
 | 
				
			||||||
        <div class="">شماره درخواست: {{ instance.code }}</div>
 | 
					        <div class="">شماره درخواست: {{ instance.code }}</div>
 | 
				
			||||||
        <div class="">تاریخ: {{ cert.jissued_at }}</div>
 | 
					        <div class="">تاریخ: {{ cert.jissued_at }}</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					@ -38,10 +39,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Header with logo and company -->
 | 
					    <!-- Header with logo and company -->
 | 
				
			||||||
    <div class="header text-center">
 | 
					    <div class="header text-center">
 | 
				
			||||||
      {% if template.company and template.company.logo %}
 | 
					      <h4 class="">{{ cert.rendered_title }}</h4>
 | 
				
			||||||
        <img src="{{ template.company.logo.url }}" alt="logo" style="max-height:90px">
 | 
					 | 
				
			||||||
      {% endif %}
 | 
					 | 
				
			||||||
      <h4 class="mt-2">{{ cert.rendered_title }}</h4>
 | 
					 | 
				
			||||||
      {% if template.company %}
 | 
					      {% if template.company %}
 | 
				
			||||||
        <div class="text-muted company-name">{{ template.company.name }}</div>
 | 
					        <div class="text-muted company-name">{{ template.company.name }}</div>
 | 
				
			||||||
      {% endif %}
 | 
					      {% endif %}
 | 
				
			||||||
| 
						 | 
					@ -51,17 +49,41 @@
 | 
				
			||||||
    <div class="body-text">
 | 
					    <div class="body-text">
 | 
				
			||||||
      {{ cert.rendered_body|safe }}
 | 
					      {{ cert.rendered_body|safe }}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <h6 class="my-2">مشخصات چاه و کنتور هوشمند</h6>
 | 
				
			||||||
    <!-- Signature -->
 | 
					    <div class="row" style="font-size: 14px;">
 | 
				
			||||||
    <div class="signature-section d-flex justify-content-end">
 | 
					      <div class="col-4">
 | 
				
			||||||
      <div class="text-center">
 | 
					        <div>موقعیت مکانی (UTM): {{ latest_report.utm_x|default:'-' }} , {{ latest_report.utm_y|default:'-' }}</div>
 | 
				
			||||||
        <div>مهر و امضای تایید کننده</div>
 | 
					        <div>نیرو محرکه چاه: {{ latest_report.driving_force|default:'-' }}</div>
 | 
				
			||||||
        <div class="text-muted">{{ template.company.name }}</div>
 | 
					        <div>نوع کنتور: {{ latest_report.get_meter_type_display|default:'-' }}</div>
 | 
				
			||||||
        {% if template.company and template.company.signature %}
 | 
					        <div>قطر لوله آبده (اینچ): {{ latest_report.discharge_pipe_diameter|default:'-' }}</div>
 | 
				
			||||||
          <img src="{{ template.company.signature.url }}" alt="seal" style="max-height:200px">
 | 
					        <div>نوع مصرف: {{ latest_report.get_usage_type_display|default:'-' }}</div>
 | 
				
			||||||
        {% endif %}
 | 
					        <div>شماره سیمکارت: {{ latest_report.sim_number|default:'-' }}</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="col-4">
 | 
				
			||||||
 | 
					        <div>سایز کنتور: {{ latest_report.meter_size|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>شماره پروانه بهرهبرداری چاه: {{ latest_report.exploitation_license_number|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>قدرت موتور: {{ latest_report.motor_power|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>نام شرکت کنتورساز: {{ latest_report.water_meter_manufacturer.name|default:'-' }}</div>
 | 
				
			||||||
 | 
					        <div>شماره سریال کنتور: {{ instance.well.water_meter_serial_number|default:'-' }}</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="col-4">
 | 
				
			||||||
 | 
					        <!-- 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:200px">
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <script>
 | 
					  <script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,9 +38,9 @@
 | 
				
			||||||
            </small>
 | 
					            </small>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="d-flex gap-2">
 | 
					          <div class="d-flex gap-2">
 | 
				
			||||||
            <a class="btn btn-outline-secondary" target="_blank" href="{% url 'certificates:certificate_print' instance.id %}">
 | 
					            <button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#printHologramModal">
 | 
				
			||||||
              <i class="bx bx-printer me-2"></i> پرینت
 | 
					              <i class="bx bx-printer me-2"></i> پرینت
 | 
				
			||||||
            </a>
 | 
					            </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
					            <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
				
			||||||
              <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
					              <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
				
			||||||
| 
						 | 
					@ -61,16 +61,33 @@
 | 
				
			||||||
                    <div>تاریخ: {{ cert.jissued_at }}</div>
 | 
					                    <div>تاریخ: {{ cert.jissued_at }}</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="text-center mb-3">
 | 
					                <div class="text-center">
 | 
				
			||||||
                  {% if template.company and template.company.logo %}
 | 
					 | 
				
			||||||
                    <img src="{{ template.company.logo.url }}" alt="logo" style="max-height:80px">
 | 
					 | 
				
			||||||
                  {% endif %}
 | 
					 | 
				
			||||||
                  <h5 class="mt-2">{{ cert.rendered_title }}</h5>
 | 
					                  <h5 class="mt-2">{{ cert.rendered_title }}</h5>
 | 
				
			||||||
                  {% if template.company %}<div class="text-muted">{{ template.company.name }}</div>{% endif %}
 | 
					                  {% if template.company %}<div class="text-muted">{{ template.company.name }}</div>{% endif %}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="mt-3" style="white-space:pre-line; line-height:1.9;">
 | 
					                <div class="mb-3" style="white-space:pre-line; line-height:1.9;">
 | 
				
			||||||
                  {{ cert.rendered_body|safe }}
 | 
					                  {{ cert.rendered_body|safe }}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					                <h6 class="mb-2">مشخصات چاه و کنتور هوشمند</h6>
 | 
				
			||||||
 | 
					                <div class="row g-2 small">
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-6">
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">موقعیت مکانی (UTM):</span><span class="fw-medium">{{ latest_report.utm_x|default:'-' }} , {{ latest_report.utm_y|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">نیرو محرکه چاه:</span><span class="fw-medium">{{ latest_report.driving_force|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">نوع کنتور:</span><span class="fw-medium">{{ latest_report.get_meter_type_display|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">قطر لوله آبده (اینچ):</span><span class="fw-medium">{{ latest_report.discharge_pipe_diameter|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">نوع مصرف:</span><span class="fw-medium">{{ latest_report.get_usage_type_display|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">شماره سیمکارت:</span><span class="fw-medium">{{ latest_report.sim_number|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-6">
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">سایز کنتور:</span><span class="fw-medium">{{ latest_report.meter_size|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">شماره پروانه بهرهبرداری چاه:</span><span class="fw-medium">{{ latest_report.exploitation_license_number|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">قدرت موتور:</span><span class="fw-medium">{{ latest_report.motor_power|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">دبی قبل از کالیبراسیون:</span><span class="fw-medium">{{ latest_report.pre_calibration_flow_rate|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">دبی بعد از کالیبراسیون:</span><span class="fw-medium">{{ latest_report.post_calibration_flow_rate|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">نام شرکت کنتورساز:</span><span class="fw-medium">{{ latest_report.water_meter_manufacturer.name|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                    <div class="d-flex gap-2"><span class="text-muted">شماره سریال کنتور:</span><span class="fw-medium">{{ instance.well.water_meter_serial_number|default:'-' }}</span></div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
                <div class="signature-section d-flex justify-content-end">
 | 
					                <div class="signature-section d-flex justify-content-end">
 | 
				
			||||||
                  <div class="text-center">
 | 
					                  <div class="text-center">
 | 
				
			||||||
                    <div>مهر و امضای تایید کننده</div>
 | 
					                    <div>مهر و امضای تایید کننده</div>
 | 
				
			||||||
| 
						 | 
					@ -103,6 +120,29 @@
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- Print Hologram Modal -->
 | 
				
			||||||
 | 
					  <div class="modal fade" id="printHologramModal" tabindex="-1" aria-hidden="true">
 | 
				
			||||||
 | 
					    <div class="modal-dialog">
 | 
				
			||||||
 | 
					      <div class="modal-content">
 | 
				
			||||||
 | 
					        <form method="post" action="{% url 'certificates:certificate_print' instance.id %}" target="_blank">
 | 
				
			||||||
 | 
					          {% csrf_token %}
 | 
				
			||||||
 | 
					          <div class="modal-header">
 | 
				
			||||||
 | 
					            <h5 class="modal-title">کد یکتا هولوگرام</h5>
 | 
				
			||||||
 | 
					            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="modal-body">
 | 
				
			||||||
 | 
					            <label class="form-label">کد هولوگرام</label>
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" name="hologram_code" value="{{ cert.hologram_code|default:'' }}" placeholder="مثال: 123456" required>
 | 
				
			||||||
 | 
					            <div class="form-text">این کد باید با کد هولوگرام روی گواهی یکسان باشد.</div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="modal-footer">
 | 
				
			||||||
 | 
					            <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
 | 
					            <button type="submit" class="btn btn-primary">ثبت و پرینت</button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,16 +52,20 @@ def certificate_step(request, instance_id, step_id):
 | 
				
			||||||
    # Ensure all previous steps are completed and invoice settled
 | 
					    # 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)
 | 
					    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()
 | 
					    incomplete = StepInstance.objects.filter(process_instance=instance, step__in=prior_steps).exclude(status='completed').exists()
 | 
				
			||||||
 | 
					    previous_step = instance.process.steps.filter(order__lt=instance.current_step.order).last() if instance.current_step else None
 | 
				
			||||||
 | 
					    prev_si = StepInstance.objects.filter(process_instance=instance, step=previous_step).first() if previous_step else None 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if incomplete:
 | 
					    if incomplete and not prev_si.status == 'approved':
 | 
				
			||||||
        messages.error(request, 'ابتدا همه مراحل قبلی را تکمیل کنید')
 | 
					        messages.error(request, 'ابتدا همه مراحل قبلی را تکمیل کنید')
 | 
				
			||||||
        return redirect('processes:request_list')
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
    inv = Invoice.objects.filter(process_instance=instance).first()
 | 
					    inv = Invoice.objects.filter(process_instance=instance).first()
 | 
				
			||||||
    if inv:
 | 
					    if inv:
 | 
				
			||||||
        inv.calculate_totals()
 | 
					        if prev_si and not prev_si.status == 'approved':
 | 
				
			||||||
        if inv.remaining_amount != 0:
 | 
					            inv.calculate_totals()
 | 
				
			||||||
            messages.error(request, 'مانده فاکتور باید صفر باشد')
 | 
					            if inv.remaining_amount != 0:
 | 
				
			||||||
            return redirect('processes:request_list')
 | 
					                messages.error(request, 'مانده فاکتور باید صفر باشد')
 | 
				
			||||||
 | 
					                return redirect('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
 | 
					    template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
 | 
				
			||||||
    if not template:
 | 
					    if not template:
 | 
				
			||||||
| 
						 | 
					@ -117,6 +121,8 @@ def certificate_step(request, instance_id, step_id):
 | 
				
			||||||
        instance.save()
 | 
					        instance.save()
 | 
				
			||||||
        return redirect('processes:instance_summary', instance_id=instance.id)
 | 
					        return redirect('processes:instance_summary', instance_id=instance.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # latest installation report for details
 | 
				
			||||||
 | 
					    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
				
			||||||
    return render(request, 'certificates/step.html', {
 | 
					    return render(request, 'certificates/step.html', {
 | 
				
			||||||
        'instance': instance,
 | 
					        'instance': instance,
 | 
				
			||||||
        'template': template,
 | 
					        'template': template,
 | 
				
			||||||
| 
						 | 
					@ -124,6 +130,7 @@ def certificate_step(request, instance_id, step_id):
 | 
				
			||||||
        'previous_step': previous_step,
 | 
					        'previous_step': previous_step,
 | 
				
			||||||
        'next_step': next_step,
 | 
					        'next_step': next_step,
 | 
				
			||||||
        'step': step,
 | 
					        'step': step,
 | 
				
			||||||
 | 
					        'latest_report': latest_report,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,11 +138,32 @@ def certificate_step(request, instance_id, step_id):
 | 
				
			||||||
def certificate_print(request, instance_id):
 | 
					def certificate_print(request, instance_id):
 | 
				
			||||||
    instance = get_scoped_instance_or_404(request, instance_id)
 | 
					    instance = get_scoped_instance_or_404(request, instance_id)
 | 
				
			||||||
    cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
 | 
					    cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
 | 
				
			||||||
 | 
					    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
				
			||||||
 | 
					    if request.method == 'POST':
 | 
				
			||||||
 | 
					        # Save/update hologram code then print
 | 
				
			||||||
 | 
					        code = (request.POST.get('hologram_code') or '').strip()
 | 
				
			||||||
 | 
					        if cert:
 | 
				
			||||||
 | 
					            if code:
 | 
				
			||||||
 | 
					                cert.hologram_code = code
 | 
				
			||||||
 | 
					                cert.save(update_fields=['hologram_code'])
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            template = CertificateTemplate.objects.filter(is_active=True).order_by('-created').first()
 | 
				
			||||||
 | 
					            if template:
 | 
				
			||||||
 | 
					                title, body = _render_template(template, instance)
 | 
				
			||||||
 | 
					                cert = CertificateInstance.objects.create(process_instance=instance, template=template, rendered_title=title, rendered_body=body, hologram_code=code or None)
 | 
				
			||||||
 | 
					        # proceed to rendering page after saving code
 | 
				
			||||||
 | 
					        return render(request, 'certificates/print.html', {
 | 
				
			||||||
 | 
					            'instance': instance,
 | 
				
			||||||
 | 
					            'cert': cert,
 | 
				
			||||||
 | 
					            'template': cert.template if cert else None,
 | 
				
			||||||
 | 
					            'latest_report': latest_report,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    template = cert.template if cert else None
 | 
					    template = cert.template if cert else None
 | 
				
			||||||
    return render(request, 'certificates/print.html', {
 | 
					    return render(request, 'certificates/print.html', {
 | 
				
			||||||
        'instance': instance,
 | 
					        'instance': instance,
 | 
				
			||||||
        'cert': cert,
 | 
					        'cert': cert,
 | 
				
			||||||
        'template': template,
 | 
					        'template': template,
 | 
				
			||||||
 | 
					        'latest_report': latest_report,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,11 @@ class UserRoles(Enum):
 | 
				
			||||||
    HEADQUARTER = "hdq" # ستاد آب منطقهای
 | 
					    HEADQUARTER = "hdq" # ستاد آب منطقهای
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USER_TYPE_CHOICES = [
 | 
				
			||||||
 | 
					    ('individual', 'حقیقی'),
 | 
				
			||||||
 | 
					    ('legal', 'حقوقی'),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BANK_CHOICES = [
 | 
					BANK_CHOICES = [
 | 
				
			||||||
    ('mellat', 'بانک ملت'),
 | 
					    ('mellat', 'بانک ملت'),
 | 
				
			||||||
    ('saman', 'بانک سامان'),
 | 
					    ('saman', 'بانک سامان'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,9 +96,8 @@
 | 
				
			||||||
              <span></span>
 | 
					              <span></span>
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
            {% if next_step %}
 | 
					            {% if next_step %}
 | 
				
			||||||
              {% if is_broker %}
 | 
					              {% if is_broker and step_instance.status != 'completed' %}
 | 
				
			||||||
                <button type="submit" class="btn btn-primary">تایید و بعدی
 | 
					                <button type="submit" class="btn btn-primary">تایید
 | 
				
			||||||
                  <i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
 | 
					 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
              {% else %}
 | 
					              {% else %}
 | 
				
			||||||
              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
					              <a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					from django.db.models.query import FlatValuesListIterable
 | 
				
			||||||
from django.shortcuts import render, get_object_or_404, redirect
 | 
					from django.shortcuts import render, get_object_or_404, redirect
 | 
				
			||||||
from django.contrib.auth.decorators import login_required
 | 
					from django.contrib.auth.decorators import login_required
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
| 
						 | 
					@ -28,6 +29,9 @@ def build_contract_context(instance: ProcessInstance) -> dict:
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        latest_payment_date = None
 | 
					        latest_payment_date = None
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    individual = True if profile and profile.user_type == 'individual' else False
 | 
				
			||||||
 | 
					    company_national_id = profile.company_national_id if profile and profile.user_type == 'legal' else None
 | 
				
			||||||
 | 
					    company_name = profile.company_name if profile and profile.user_type == 'legal' else None
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{representative.get_full_name() if representative else ''}</span>"),
 | 
					        '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>"),
 | 
					        'registration_number': mark_safe(f"<span class=\"fw-bold\">{instance.broker.company.registration_number if instance.broker and instance.broker.company else ''}</span>"),
 | 
				
			||||||
| 
						 | 
					@ -48,6 +52,11 @@ def build_contract_context(instance: ProcessInstance) -> dict:
 | 
				
			||||||
        'bank_name': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.get_bank_name_display() 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_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 '',
 | 
					        'prepayment_date': mark_safe(f"<span class=\"fw-bold\">{jalali_converter2(latest_payment_date)}</span>") if latest_payment_date else '',
 | 
				
			||||||
 | 
					        'user_type': mark_safe(f"<span>{profile.get_user_type_display() if profile else ''}</span>"),
 | 
				
			||||||
 | 
					        'individual': individual,
 | 
				
			||||||
 | 
					        'company_national_id': mark_safe(f"<span class=\"fw-bold\">{company_national_id if company_national_id else ''}</span>"),
 | 
				
			||||||
 | 
					        'company_name': mark_safe(f"<span class=\"fw-bold\">{company_name if company_name else ''}</span>"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,6 +68,8 @@ def contract_step(request, instance_id, step_id):
 | 
				
			||||||
    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
					    previous_step = instance.process.steps.filter(order__lt=step.order).last()
 | 
				
			||||||
    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
					    next_step = instance.process.steps.filter(order__gt=step.order).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    step_instance = StepInstance.objects.filter(process_instance=instance, step=step).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    profile = getattr(request.user, 'profile', None)
 | 
					    profile = getattr(request.user, 'profile', None)
 | 
				
			||||||
    is_broker = False
 | 
					    is_broker = False
 | 
				
			||||||
    can_view_contract_body = True
 | 
					    can_view_contract_body = True
 | 
				
			||||||
| 
						 | 
					@ -93,15 +104,16 @@ def contract_step(request, instance_id, step_id):
 | 
				
			||||||
    if request.method == 'POST':
 | 
					    if request.method == 'POST':
 | 
				
			||||||
        if not is_broker:
 | 
					        if not is_broker:
 | 
				
			||||||
            return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
					            return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
 | 
				
			||||||
        StepInstance.objects.update_or_create(
 | 
					        step_instance, _ = StepInstance.objects.update_or_create(
 | 
				
			||||||
            process_instance=instance,
 | 
					            process_instance=instance,
 | 
				
			||||||
            step=step,
 | 
					            step=step,
 | 
				
			||||||
            defaults={'status': 'completed', 'completed_at': timezone.now()}
 | 
					            defaults={'status': 'completed', 'completed_at': timezone.now()}
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if next_step:
 | 
					        if next_step:
 | 
				
			||||||
            instance.current_step = next_step
 | 
					            # instance.current_step = next_step
 | 
				
			||||||
            instance.save()
 | 
					            instance.save()
 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					            # return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
 | 
				
			||||||
        return redirect('processes:request_list')
 | 
					        return redirect('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return render(request, 'contracts/contract_step.html', {
 | 
					    return render(request, 'contracts/contract_step.html', {
 | 
				
			||||||
| 
						 | 
					@ -113,6 +125,7 @@ def contract_step(request, instance_id, step_id):
 | 
				
			||||||
        'next_step': next_step,
 | 
					        'next_step': next_step,
 | 
				
			||||||
        'is_broker': is_broker,
 | 
					        'is_broker': is_broker,
 | 
				
			||||||
        'can_view_contract_body': can_view_contract_body,
 | 
					        'can_view_contract_body': can_view_contract_body,
 | 
				
			||||||
 | 
					        'step_instance': step_instance,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -21,10 +21,40 @@ class InstallationItemChangeInline(admin.TabularInline):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(InstallationReport)
 | 
					@admin.register(InstallationReport)
 | 
				
			||||||
class InstallationReportAdmin(admin.ModelAdmin):
 | 
					class InstallationReportAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_display = ('assignment', 'visited_date', 'new_water_meter_serial', 'seal_number', 'is_meter_suspicious', 'approved', 'created')
 | 
					    list_display = (
 | 
				
			||||||
    list_filter = ('is_meter_suspicious', 'approved', 'visited_date')
 | 
					        'assignment', 'visited_date', 'meter_type', 'meter_size', 'water_meter_manufacturer',
 | 
				
			||||||
    search_fields = ('assignment__process_instance__code', 'new_water_meter_serial', 'seal_number')
 | 
					        'discharge_pipe_diameter', 'usage_type', 'exploitation_license_number',
 | 
				
			||||||
 | 
					        'motor_power', 'pre_calibration_flow_rate', 'post_calibration_flow_rate',
 | 
				
			||||||
 | 
					        'new_water_meter_serial', 'seal_number', 'sim_number',
 | 
				
			||||||
 | 
					        'is_meter_suspicious', 'approved', 'created'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    list_filter = ('is_meter_suspicious', 'approved', 'visited_date', 'meter_type', 'usage_type', 'water_meter_manufacturer')
 | 
				
			||||||
 | 
					    search_fields = (
 | 
				
			||||||
 | 
					        'assignment__process_instance__code', 'new_water_meter_serial', 'seal_number', 'exploitation_license_number', 'sim_number'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    inlines = [InstallationPhotoInline, InstallationItemChangeInline]
 | 
					    inlines = [InstallationPhotoInline, InstallationItemChangeInline]
 | 
				
			||||||
 | 
					    fieldsets = (
 | 
				
			||||||
 | 
					        ('زمان و تایید', {
 | 
				
			||||||
 | 
					            'fields': ('visited_date', 'approved', 'approved_at')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('کنتور و سازنده', {
 | 
				
			||||||
 | 
					            'fields': (
 | 
				
			||||||
 | 
					                'meter_type', 'meter_size', 'water_meter_manufacturer', 'new_water_meter_serial', 'seal_number', 'is_meter_suspicious', 'sim_number'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('مشخصات هیدرولیکی', {
 | 
				
			||||||
 | 
					            'fields': ('discharge_pipe_diameter', 'pre_calibration_flow_rate', 'post_calibration_flow_rate')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('کاربری و مجوز', {
 | 
				
			||||||
 | 
					            'fields': ('usage_type', 'exploitation_license_number')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('توان و محرکه', {
 | 
				
			||||||
 | 
					            'fields': ('driving_force', 'motor_power')
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ('توضیحات', {
 | 
				
			||||||
 | 
					            'fields': ('description',)
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(InstallationPhoto)
 | 
					@admin.register(InstallationPhoto)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										208
									
								
								installations/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								installations/forms.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,208 @@
 | 
				
			||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
 | 
					from .models import InstallationReport
 | 
				
			||||||
 | 
					from wells.models import WaterMeterManufacturer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InstallationReportForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    # Additional fields for manufacturer handling
 | 
				
			||||||
 | 
					    new_manufacturer = forms.CharField(
 | 
				
			||||||
 | 
					        max_length=100, 
 | 
				
			||||||
 | 
					        required=False, 
 | 
				
			||||||
 | 
					        widget=forms.TextInput(attrs={
 | 
				
			||||||
 | 
					            'class': 'form-control',
 | 
				
			||||||
 | 
					            'placeholder': 'شرکت سازنده جدید',
 | 
				
			||||||
 | 
					            'style': 'display:none;'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = InstallationReport
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'visited_date', 'new_water_meter_serial', 'seal_number',
 | 
				
			||||||
 | 
					            'utm_x', 'utm_y', 'meter_type', 'meter_size',
 | 
				
			||||||
 | 
					            'discharge_pipe_diameter', 'usage_type', 'exploitation_license_number',
 | 
				
			||||||
 | 
					            'motor_power', 'pre_calibration_flow_rate', 'post_calibration_flow_rate',
 | 
				
			||||||
 | 
					            'water_meter_manufacturer', 'sim_number', 'driving_force',
 | 
				
			||||||
 | 
					            'is_meter_suspicious', 'description'
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            'visited_date': forms.DateInput(attrs={
 | 
				
			||||||
 | 
					                'type': 'date',
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'required': True
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'new_water_meter_serial': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'seal_number': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'utm_x': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'step': '1'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'utm_y': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'step': '1'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'meter_type': forms.Select(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-select'
 | 
				
			||||||
 | 
					            }, choices=[
 | 
				
			||||||
 | 
					                ('', 'انتخاب کنید'),
 | 
				
			||||||
 | 
					                ('smart', 'هوشمند (آبی/برق)'),
 | 
				
			||||||
 | 
					                ('volumetric', 'حجمی')
 | 
				
			||||||
 | 
					            ]),
 | 
				
			||||||
 | 
					            'meter_size': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'discharge_pipe_diameter': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'min': '0',
 | 
				
			||||||
 | 
					                'step': '1'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'usage_type': forms.Select(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-select'
 | 
				
			||||||
 | 
					            }, choices=[
 | 
				
			||||||
 | 
					                ('', 'انتخاب کنید'),
 | 
				
			||||||
 | 
					                ('domestic', 'شرب و خدمات'),
 | 
				
			||||||
 | 
					                ('agriculture', 'کشاورزی'),
 | 
				
			||||||
 | 
					                ('industrial', 'صنعتی')
 | 
				
			||||||
 | 
					            ]),
 | 
				
			||||||
 | 
					            'exploitation_license_number': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'required': True
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'motor_power': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'min': '0',
 | 
				
			||||||
 | 
					                'step': '1'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'pre_calibration_flow_rate': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'min': '0',
 | 
				
			||||||
 | 
					                'step': '0.01'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'post_calibration_flow_rate': forms.NumberInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'min': '0',
 | 
				
			||||||
 | 
					                'step': '0.01'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'water_meter_manufacturer': forms.Select(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-select',
 | 
				
			||||||
 | 
					                'id': 'id_water_meter_manufacturer'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'sim_number': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'driving_force': forms.TextInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'is_meter_suspicious': forms.CheckboxInput(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-check-input',
 | 
				
			||||||
 | 
					                'id': 'id_is_meter_suspicious'
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            'description': forms.Textarea(attrs={
 | 
				
			||||||
 | 
					                'class': 'form-control',
 | 
				
			||||||
 | 
					                'rows': 3
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        labels = {
 | 
				
			||||||
 | 
					            'visited_date': 'تاریخ مراجعه',
 | 
				
			||||||
 | 
					            'new_water_meter_serial': 'سریال کنتور جدید',
 | 
				
			||||||
 | 
					            'seal_number': 'شماره پلمپ',
 | 
				
			||||||
 | 
					            'utm_x': 'UTM X',
 | 
				
			||||||
 | 
					            'utm_y': 'UTM Y',
 | 
				
			||||||
 | 
					            'meter_type': 'نوع کنتور',
 | 
				
			||||||
 | 
					            'meter_size': 'سایز کنتور',
 | 
				
			||||||
 | 
					            'discharge_pipe_diameter': 'قطر لوله آبده (اینچ)',
 | 
				
			||||||
 | 
					            'usage_type': 'نوع مصرف',
 | 
				
			||||||
 | 
					            'exploitation_license_number': 'شماره پروانه بهرهبرداری',
 | 
				
			||||||
 | 
					            'motor_power': 'قدرت موتور (کیلووات ساعت)',
 | 
				
			||||||
 | 
					            'pre_calibration_flow_rate': 'دبی قبل از کالیبراسیون (لیتر بر ثانیه)',
 | 
				
			||||||
 | 
					            'post_calibration_flow_rate': 'دبی بعد از کالیبراسیون (لیتر بر ثانیه)',
 | 
				
			||||||
 | 
					            'water_meter_manufacturer': 'شرکت سازنده کنتور',
 | 
				
			||||||
 | 
					            'sim_number': 'شماره سیمکارت',
 | 
				
			||||||
 | 
					            'driving_force': 'نیرو محرکه چاه',
 | 
				
			||||||
 | 
					            'is_meter_suspicious': 'کنتور مشکوک است',
 | 
				
			||||||
 | 
					            'description': 'توضیحات'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self.user_is_installer = kwargs.pop('user_is_installer', False)
 | 
				
			||||||
 | 
					        self.instance_well = kwargs.pop('instance_well', None)
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Set manufacturer choices
 | 
				
			||||||
 | 
					        manufacturers = WaterMeterManufacturer.objects.filter(is_deleted=False)
 | 
				
			||||||
 | 
					        manufacturer_choices = [('', 'انتخاب شرکت سازنده')]
 | 
				
			||||||
 | 
					        manufacturer_choices.extend([(m.id, m.name) for m in manufacturers])
 | 
				
			||||||
 | 
					        self.fields['water_meter_manufacturer'].choices = manufacturer_choices
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Pre-fill UTM from well if available and no existing report data
 | 
				
			||||||
 | 
					        if self.instance_well and not self.instance.pk:
 | 
				
			||||||
 | 
					            if self.instance_well.utm_x:
 | 
				
			||||||
 | 
					                self.initial['utm_x'] = self.instance_well.utm_x
 | 
				
			||||||
 | 
					            if self.instance_well.utm_y:
 | 
				
			||||||
 | 
					                self.initial['utm_y'] = self.instance_well.utm_y
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Disable fields for non-installers
 | 
				
			||||||
 | 
					        if not self.user_is_installer:
 | 
				
			||||||
 | 
					            for field_name, field in self.fields.items():
 | 
				
			||||||
 | 
					                if field_name != 'new_manufacturer':  # Keep this always disabled via CSS
 | 
				
			||||||
 | 
					                    field.widget.attrs['readonly'] = True
 | 
				
			||||||
 | 
					                    if isinstance(field.widget, (forms.Select, forms.CheckboxInput)):
 | 
				
			||||||
 | 
					                        field.widget.attrs['disabled'] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean(self):
 | 
				
			||||||
 | 
					        cleaned_data = super().clean()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Handle new manufacturer creation
 | 
				
			||||||
 | 
					        new_manufacturer = cleaned_data.get('new_manufacturer')
 | 
				
			||||||
 | 
					        water_meter_manufacturer = cleaned_data.get('water_meter_manufacturer')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if new_manufacturer and not water_meter_manufacturer:
 | 
				
			||||||
 | 
					            # Create new manufacturer
 | 
				
			||||||
 | 
					            manufacturer, created = WaterMeterManufacturer.objects.get_or_create(
 | 
				
			||||||
 | 
					                name=new_manufacturer,
 | 
				
			||||||
 | 
					                defaults={'is_deleted': False}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            cleaned_data['water_meter_manufacturer'] = manufacturer
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return cleaned_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_visited_date(self):
 | 
				
			||||||
 | 
					        visited_date = self.cleaned_data.get('visited_date')
 | 
				
			||||||
 | 
					        if not visited_date:
 | 
				
			||||||
 | 
					            raise ValidationError('تاریخ مراجعه الزامی است.')
 | 
				
			||||||
 | 
					        return visited_date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_exploitation_license_number(self):
 | 
				
			||||||
 | 
					        license_number = self.cleaned_data.get('exploitation_license_number')
 | 
				
			||||||
 | 
					        if not license_number or not license_number.strip():
 | 
				
			||||||
 | 
					            raise ValidationError('شماره پروانه بهرهبرداری الزامی است.')
 | 
				
			||||||
 | 
					        return license_number.strip()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def validate_photos(self, request_files, existing_photos, deleted_photo_ids):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Validate that at least one photo is present (either existing or newly uploaded)
 | 
				
			||||||
 | 
					        This method should be called from the view after form.is_valid()
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # Count existing photos that are not deleted
 | 
				
			||||||
 | 
					        kept_existing = 0
 | 
				
			||||||
 | 
					        if existing_photos:
 | 
				
			||||||
 | 
					            for photo in existing_photos:
 | 
				
			||||||
 | 
					                if str(photo.id) not in deleted_photo_ids:
 | 
				
			||||||
 | 
					                    kept_existing += 1
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Count new photos
 | 
				
			||||||
 | 
					        new_photos = len(request_files.getlist('photos')) if request_files else 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        total_photos = kept_existing + new_photos
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if total_photos <= 0:
 | 
				
			||||||
 | 
					            raise ValidationError('بارگذاری حداقل یک عکس الزامی است.')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-21 07:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('installations', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='utm_x',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='UTM X'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='utm_y',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='UTM Y'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,70 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-24 11:15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('installations', '0002_alter_installationreport_utm_x_and_more'),
 | 
				
			||||||
 | 
					        ('wells', '0004_remove_historicalwell_discharge_pipe_diameter_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='discharge_pipe_diameter',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قطر لوله آبده (اینچ)'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='driving_force',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='نیرو محرکه چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='meter_size',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='سایز کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='meter_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('smart', 'هوشمند (آبی/برق)'), ('volumetric', 'حجمی')], max_length=20, null=True, verbose_name='نوع کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت)قدرت موتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='post_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='pre_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='sim_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره سیمکارت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='water_meter_manufacturer',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-27 06:17
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('installations', '0003_installationreport_discharge_pipe_diameter_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت) قدرت موتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-29 10:41
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('installations', '0004_alter_installationreport_motor_power'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-29 10:47
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('installations', '0005_alter_installationreport_usage_type'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='installationreport',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					            field=models.CharField(default=1, max_length=50, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,26 @@ class InstallationReport(BaseModel):
 | 
				
			||||||
    new_water_meter_serial = models.CharField(max_length=50, null=True, blank=True, verbose_name='سریال کنتور جدید')
 | 
					    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='شماره پلمپ')
 | 
					    seal_number = models.CharField(max_length=50, null=True, blank=True, verbose_name='شماره پلمپ')
 | 
				
			||||||
    is_meter_suspicious = models.BooleanField(default=False, verbose_name='کنتور مشکوک است؟')
 | 
					    is_meter_suspicious = models.BooleanField(default=False, verbose_name='کنتور مشکوک است؟')
 | 
				
			||||||
 | 
					    METER_TYPE_CHOICES = [
 | 
				
			||||||
 | 
					        ('smart', 'هوشمند (آبی/برق)'),
 | 
				
			||||||
 | 
					        ('volumetric', 'حجمی'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    meter_type = models.CharField(max_length=20, choices=METER_TYPE_CHOICES, null=True, blank=True, verbose_name='نوع کنتور')
 | 
				
			||||||
 | 
					    meter_size = models.CharField(max_length=50, null=True, blank=True, verbose_name='سایز کنتور')
 | 
				
			||||||
 | 
					    discharge_pipe_diameter = models.PositiveIntegerField(null=True, blank=True, verbose_name='قطر لوله آبده (اینچ)')
 | 
				
			||||||
 | 
					    USAGE_TYPE_CHOICES = [
 | 
				
			||||||
 | 
					        ('domestic', 'شرب و خدمات'),
 | 
				
			||||||
 | 
					        ('agriculture', 'کشاورزی'),
 | 
				
			||||||
 | 
					        ('industrial', 'صنعتی'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    usage_type = models.CharField(max_length=20, choices=USAGE_TYPE_CHOICES, null=True, verbose_name='نوع مصرف')
 | 
				
			||||||
 | 
					    exploitation_license_number = models.CharField(max_length=50, verbose_name='شماره پروانه بهرهبرداری چاه')
 | 
				
			||||||
 | 
					    motor_power = models.PositiveIntegerField(null=True, blank=True, verbose_name='(کیلووات ساعت) قدرت موتور')
 | 
				
			||||||
 | 
					    pre_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون')
 | 
				
			||||||
 | 
					    post_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون')
 | 
				
			||||||
 | 
					    water_meter_manufacturer = models.ForeignKey('wells.WaterMeterManufacturer', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='شرکت سازنده کنتور آب')
 | 
				
			||||||
 | 
					    sim_number = models.CharField(max_length=20, null=True, blank=True, verbose_name='شماره سیمکارت')
 | 
				
			||||||
 | 
					    driving_force = models.CharField(max_length=50, null=True, blank=True, verbose_name='نیرو محرکه چاه')
 | 
				
			||||||
    utm_x = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, verbose_name='UTM X')
 | 
					    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')
 | 
					    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='توضیحات')
 | 
					    description = models.TextField(blank=True, verbose_name='توضیحات')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@
 | 
				
			||||||
        <div class="bs-stepper-content">
 | 
					        <div class="bs-stepper-content">
 | 
				
			||||||
          {% if report and not edit_mode %}
 | 
					          {% if report and not edit_mode %}
 | 
				
			||||||
          <div class="mb-3 text-end">
 | 
					          <div class="mb-3 text-end">
 | 
				
			||||||
            {% if user_is_installer %}
 | 
					            {% if user_is_installer and not report.approved %}
 | 
				
			||||||
              <a href="?edit=1" class="btn btn-primary">
 | 
					              <a href="?edit=1" class="btn btn-primary">
 | 
				
			||||||
                <i class="bx bx-edit bx-sm me-2"></i>
 | 
					                <i class="bx bx-edit bx-sm me-2"></i>
 | 
				
			||||||
                ویرایش گزارش نصب
 | 
					                ویرایش گزارش نصب
 | 
				
			||||||
| 
						 | 
					@ -86,11 +86,23 @@
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال جدید: {{ report.new_water_meter_serial|default:'-' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال جدید: {{ report.new_water_meter_serial|default:'-' }}</p>
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ report.seal_number|default:'-' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ report.seal_number|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-chip bx-sm me-2"></i>نوع کنتور: {{ report.get_meter_type_display|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-ruler bx-sm me-2"></i>سایز کنتور: {{ report.meter_size|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-tachometer bx-sm me-2"></i>قطر لوله آبده (اینچ): {{ report.discharge_pipe_diameter|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-building bx-sm me-2"></i>سازنده کنتور: {{ report.water_meter_manufacturer|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-sim-card bx-sm me-2"></i>شماره سیمکارت: {{ report.sim_number|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-cog bx-sm me-2"></i>نیرو محرکه چاه: {{ report.driving_force|default:'-' }}</p>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="col-md-6">
 | 
					                <div class="col-md-6">
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ report.utm_x|default:'-' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ report.utm_x|default:'-' }}</p>
 | 
				
			||||||
                  <p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ report.utm_y|default:'-' }}</p>
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ report.utm_y|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع مصرف: {{ report.get_usage_type_display|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه بهرهبرداری: {{ report.exploitation_license_number|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-bolt-circle bx-sm me-2"></i>(کیلووات ساعت)قدرت موتور: {{ report.motor_power|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی قبل کالیبراسیون: {{ report.pre_calibration_flow_rate|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  <p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی بعد کالیبراسیون: {{ report.post_calibration_flow_rate|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              {% if report.description %}
 | 
					              {% if report.description %}
 | 
				
			||||||
| 
						 | 
					@ -155,11 +167,18 @@
 | 
				
			||||||
          <div class="card border mt-2">
 | 
					          <div class="card border mt-2">
 | 
				
			||||||
            <div class="card-header d-flex justify-content-between align-items-center">
 | 
					            <div class="card-header d-flex justify-content-between align-items-center">
 | 
				
			||||||
              <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
					              <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
				
			||||||
              {% if user_can_approve %}
 | 
					              {% if can_approve_reject %}
 | 
				
			||||||
              <div class="d-flex gap-2">
 | 
					                {% if current_user_has_decided %}
 | 
				
			||||||
                <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal">تایید</button>
 | 
					                <div class="d-flex gap-2">
 | 
				
			||||||
                <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
 | 
					                  <button type="button" class="btn btn-success btn-sm" disabled>تایید</button>
 | 
				
			||||||
              </div>
 | 
					                  <button type="button" class="btn btn-danger btn-sm" disabled>رد</button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					                  <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 %}
 | 
				
			||||||
              {% endif %}
 | 
					              {% endif %}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="card-body py-3">
 | 
					            <div class="card-body py-3">
 | 
				
			||||||
| 
						 | 
					@ -213,41 +232,149 @@
 | 
				
			||||||
          {% if user_is_installer %}
 | 
					          {% if user_is_installer %}
 | 
				
			||||||
          <!-- Installation Report Form -->
 | 
					          <!-- Installation Report Form -->
 | 
				
			||||||
          <form method="post" enctype="multipart/form-data" id="installation-report-form">
 | 
					          <form method="post" enctype="multipart/form-data" id="installation-report-form">
 | 
				
			||||||
            {% csrf_token %}
 | 
					            {% csrf_token %}         
 | 
				
			||||||
            <div class="mb-3">
 | 
					            <div class="mb-3">
 | 
				
			||||||
              <div class="">
 | 
					              <div class="">
 | 
				
			||||||
                <div class="row g-3">
 | 
					                <div class="row g-3">
 | 
				
			||||||
                  <div class="col-md-3">
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
                    <label class="form-label">تاریخ مراجعه</label>
 | 
					                    {{ form.visited_date.label_tag }}
 | 
				
			||||||
                    <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 %}">
 | 
					                    <!-- Custom date picker handling -->
 | 
				
			||||||
                    <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 %}">
 | 
					                    <input type="text" id="id_visited_date_display" class="form-control{% if form.visited_date.errors %} is-invalid{% endif %}" 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' }}{% elif form.visited_date.value %}{{ form.visited_date.value|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' }}{% elif form.visited_date.value %}{{ form.visited_date.value }}{% endif %}">
 | 
				
			||||||
 | 
					                    {% if form.visited_date.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.visited_date.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-md-3">
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
                    <label class="form-label">سریال کنتور جدید</label>
 | 
					                    {{ form.new_water_meter_serial.label_tag }}
 | 
				
			||||||
                    <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 %}>
 | 
					                    {{ form.new_water_meter_serial }}
 | 
				
			||||||
 | 
					                    {% if form.new_water_meter_serial.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.new_water_meter_serial.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-md-3">
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
                    <label class="form-label">شماره پلمپ</label>
 | 
					                    {{ form.seal_number.label_tag }}
 | 
				
			||||||
                    <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 %}>
 | 
					                    {{ form.seal_number }}
 | 
				
			||||||
 | 
					                    {% if form.seal_number.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.seal_number.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.utm_x.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.utm_x }}
 | 
				
			||||||
 | 
					                    {% if form.utm_x.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.utm_x.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.utm_y.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.utm_y }}
 | 
				
			||||||
 | 
					                    {% if form.utm_y.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.utm_y.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.meter_type.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.meter_type }}
 | 
				
			||||||
 | 
					                    {% if form.meter_type.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.meter_type.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.meter_size.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.meter_size }}
 | 
				
			||||||
 | 
					                    {% if form.meter_size.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.meter_size.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.discharge_pipe_diameter.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.discharge_pipe_diameter }}
 | 
				
			||||||
 | 
					                    {% if form.discharge_pipe_diameter.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.discharge_pipe_diameter.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.usage_type.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.usage_type }}
 | 
				
			||||||
 | 
					                    {% if form.usage_type.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.usage_type.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.exploitation_license_number.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.exploitation_license_number }}
 | 
				
			||||||
 | 
					                    {% if form.exploitation_license_number.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.exploitation_license_number.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.motor_power.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.motor_power }}
 | 
				
			||||||
 | 
					                    {% if form.motor_power.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.motor_power.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.pre_calibration_flow_rate.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.pre_calibration_flow_rate }}
 | 
				
			||||||
 | 
					                    {% if form.pre_calibration_flow_rate.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.pre_calibration_flow_rate.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.post_calibration_flow_rate.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.post_calibration_flow_rate }}
 | 
				
			||||||
 | 
					                    {% if form.post_calibration_flow_rate.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.post_calibration_flow_rate.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.water_meter_manufacturer.label_tag }}
 | 
				
			||||||
 | 
					                    <div class="input-group">
 | 
				
			||||||
 | 
					                      {{ form.water_meter_manufacturer }}
 | 
				
			||||||
 | 
					                      {{ form.new_manufacturer }}
 | 
				
			||||||
 | 
					                      {% if user_is_installer %}
 | 
				
			||||||
 | 
					                      <button class="btn btn-outline-primary" type="button" id="btnToggleManufacturer"><i class="bx bx-plus"></i></button>
 | 
				
			||||||
 | 
					                      {% endif %}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {% if form.water_meter_manufacturer.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.water_meter_manufacturer.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                    {% if form.new_manufacturer.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.new_manufacturer.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.sim_number.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.sim_number }}
 | 
				
			||||||
 | 
					                    {% if form.sim_number.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.sim_number.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-md-3">
 | 
				
			||||||
 | 
					                    {{ form.driving_force.label_tag }}
 | 
				
			||||||
 | 
					                    {{ form.driving_force }}
 | 
				
			||||||
 | 
					                    {% if form.driving_force.errors %}
 | 
				
			||||||
 | 
					                      <div class="invalid-feedback">{{ form.driving_force.errors.0 }}</div>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-md-3 d-flex align-items-end">
 | 
					                  <div class="col-md-3 d-flex align-items-end">
 | 
				
			||||||
                    <div class="form-check">
 | 
					                    <div class="form-check">
 | 
				
			||||||
                      <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 %}>
 | 
					                      {{ form.is_meter_suspicious }}
 | 
				
			||||||
                      <label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
 | 
					                      {{ form.is_meter_suspicious.label_tag }}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                    {% if form.is_meter_suspicious.errors %}
 | 
				
			||||||
                  <div class="col-md-3">
 | 
					                      <div class="invalid-feedback">{{ form.is_meter_suspicious.errors.0 }}</div>
 | 
				
			||||||
                    <label class="form-label">UTM X</label>
 | 
					                    {% 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="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>
 | 
					                </div>
 | 
				
			||||||
                <div class="my-3">
 | 
					                <div class="my-3">
 | 
				
			||||||
                  <label class="form-label">توضیحات (اختیاری)</label>
 | 
					                  {{ form.description.label_tag }}
 | 
				
			||||||
                  <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>
 | 
					                  {{ form.description }}
 | 
				
			||||||
 | 
					                  {% if form.description.errors %}
 | 
				
			||||||
 | 
					                    <div class="invalid-feedback">{{ form.description.errors.0 }}</div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="mb-3">
 | 
					                <div class="mb-3">
 | 
				
			||||||
                  <div class="d-flex justify-content-between align-items-center">
 | 
					                  <div class="d-flex justify-content-between align-items-center">
 | 
				
			||||||
| 
						 | 
					@ -284,7 +411,7 @@
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="card-body">
 | 
					              <div class="card-body">
 | 
				
			||||||
                <div class="row g-3">
 | 
					                <div class="row g-3">
 | 
				
			||||||
                  <div class="col-12 mb-4">
 | 
					                  <div class="col-12 mb-4 d-none">
 | 
				
			||||||
                    <h6 class="mb-2">اقلام انتخابشده قبلی <small class="text-muted">(برای حذف در نصب تیک بزنید)</small></h6>
 | 
					                    <h6 class="mb-2">اقلام انتخابشده قبلی <small class="text-muted">(برای حذف در نصب تیک بزنید)</small></h6>
 | 
				
			||||||
                    <div class="table-responsive">
 | 
					                    <div class="table-responsive">
 | 
				
			||||||
                      <table class="table table-sm align-middle">
 | 
					                      <table class="table table-sm align-middle">
 | 
				
			||||||
| 
						 | 
					@ -321,7 +448,6 @@
 | 
				
			||||||
                      </table>
 | 
					                      </table>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <hr>
 | 
					 | 
				
			||||||
                  <div class="col-12">
 | 
					                  <div class="col-12">
 | 
				
			||||||
                    <h6 class="mb-2">افزودن اقلام جدید</h6>
 | 
					                    <h6 class="mb-2">افزودن اقلام جدید</h6>
 | 
				
			||||||
                    <div class="table-responsive">
 | 
					                    <div class="table-responsive">
 | 
				
			||||||
| 
						 | 
					@ -513,6 +639,20 @@
 | 
				
			||||||
        display.scrollIntoView({behavior:'smooth', block:'center'});
 | 
					        display.scrollIntoView({behavior:'smooth', block:'center'});
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      // Require at least one photo: either existing (not marked for deletion) or newly added
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        var keptExisting = 0;
 | 
				
			||||||
 | 
					        document.querySelectorAll('input[id^="del-photo-"]').forEach(function(inp){
 | 
				
			||||||
 | 
					          if (String(inp.value) !== '1') keptExisting += 1;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        var newFiles = document.querySelectorAll('#photoInputs input[type="file"]').length;
 | 
				
			||||||
 | 
					        if ((keptExisting + newFiles) <= 0) {
 | 
				
			||||||
 | 
					          ev.preventDefault(); ev.stopPropagation();
 | 
				
			||||||
 | 
					          showToast('بارگذاری حداقل یک عکس الزامی است', 'danger');
 | 
				
			||||||
 | 
					          (document.getElementById('btnAddPhoto') || form).scrollIntoView({behavior:'smooth', block:'center'});
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch(_) {}
 | 
				
			||||||
      try { sessionStorage.setItem('install_report_saved', '1'); } catch(_) {}
 | 
					      try { sessionStorage.setItem('install_report_saved', '1'); } catch(_) {}
 | 
				
			||||||
    }, false);
 | 
					    }, false);
 | 
				
			||||||
    // on load, if saved flag exists, show toast
 | 
					    // on load, if saved flag exists, show toast
 | 
				
			||||||
| 
						 | 
					@ -568,6 +708,36 @@
 | 
				
			||||||
    if (btnAddPhoto) btnAddPhoto.addEventListener('click', createPhotoInput);
 | 
					    if (btnAddPhoto) btnAddPhoto.addEventListener('click', createPhotoInput);
 | 
				
			||||||
  })();
 | 
					  })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Toggle manufacturer select/input (like request_list)
 | 
				
			||||||
 | 
					  (function(){
 | 
				
			||||||
 | 
					    const $select = $('#id_water_meter_manufacturer');
 | 
				
			||||||
 | 
					    const $input = $('#id_new_manufacturer');
 | 
				
			||||||
 | 
					    const $btn = $('#btnToggleManufacturer');
 | 
				
			||||||
 | 
					    if (!$select.length || !$btn.length) return;
 | 
				
			||||||
 | 
					    $btn.on('click', function(){
 | 
				
			||||||
 | 
					      if ($select.is(':visible')) {
 | 
				
			||||||
 | 
					        $select.hide();
 | 
				
			||||||
 | 
					        $input.show().focus();
 | 
				
			||||||
 | 
					        $btn.html('<i class="bx bx-check"></i>');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // When switching back, if input has value, append it as selected option
 | 
				
			||||||
 | 
					        const val = ($input.val() || '').trim();
 | 
				
			||||||
 | 
					        if (val) {
 | 
				
			||||||
 | 
					          // Add a temporary option with value prefixed 'new:' to be handled server-side
 | 
				
			||||||
 | 
					          const exists = $select.find('option').filter(function(){ return $(this).text().trim() === val; }).length > 0;
 | 
				
			||||||
 | 
					          if (!exists) {
 | 
				
			||||||
 | 
					            const opt = $('<option></option>').val('').text(val);
 | 
				
			||||||
 | 
					            $select.append(opt);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          $select.val('');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $input.hide();
 | 
				
			||||||
 | 
					        $select.show();
 | 
				
			||||||
 | 
					        $btn.html('<i class="bx bx-plus"></i>');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Mark delete for existing photos
 | 
					  // Mark delete for existing photos
 | 
				
			||||||
  function markDeletePhoto(id){
 | 
					  function markDeletePhoto(id){
 | 
				
			||||||
    const hidden = document.getElementById('del-photo-' + id);
 | 
					    const hidden = document.getElementById('del-photo-' + id);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,12 +3,15 @@ from django.contrib.auth.decorators import login_required
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from accounts.models import Profile
 | 
					from accounts.models import Profile
 | 
				
			||||||
from common.consts import UserRoles
 | 
					from common.consts import UserRoles
 | 
				
			||||||
from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval
 | 
					from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval
 | 
				
			||||||
from accounts.models import Role
 | 
					from accounts.models import Role
 | 
				
			||||||
from invoices.models import Item, Quote, QuoteItem
 | 
					from invoices.models import Item, Quote, QuoteItem
 | 
				
			||||||
 | 
					from wells.models import WaterMeterManufacturer
 | 
				
			||||||
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
 | 
					from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
 | 
				
			||||||
 | 
					from .forms import InstallationReportForm
 | 
				
			||||||
from decimal import Decimal, InvalidOperation
 | 
					from decimal import Decimal, InvalidOperation
 | 
				
			||||||
from processes.utils import get_scoped_instance_or_404
 | 
					from processes.utils import get_scoped_instance_or_404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,12 +125,9 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
    is_assigned_installer = bool(assignment and assignment.installer_id == request.user.id)
 | 
					    is_assigned_installer = bool(assignment and assignment.installer_id == request.user.id)
 | 
				
			||||||
    user_is_installer = bool(has_installer_role and is_assigned_installer)
 | 
					    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
 | 
					    edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False
 | 
				
			||||||
 | 
					    # Prevent edit mode if an approved report exists
 | 
				
			||||||
    # current quote items baseline
 | 
					    if existing_report and existing_report.approved:
 | 
				
			||||||
    quote = Quote.objects.filter(process_instance=instance).first()
 | 
					        edit_mode = False
 | 
				
			||||||
    quote_items = list(quote.items.select_related('item').all()) if quote else []
 | 
					 | 
				
			||||||
    quote_price_map = {qi.item_id: qi.unit_price for qi in quote_items}
 | 
					 | 
				
			||||||
    items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Ensure a StepInstance exists for this step
 | 
					    # Ensure a StepInstance exists for this step
 | 
				
			||||||
    step_instance, _ = StepInstance.objects.get_or_create(
 | 
					    step_instance, _ = StepInstance.objects.get_or_create(
 | 
				
			||||||
| 
						 | 
					@ -136,6 +136,177 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
        defaults={'status': 'in_progress'}
 | 
					        defaults={'status': 'in_progress'}
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # current quote items baseline
 | 
				
			||||||
 | 
					    quote = Quote.objects.filter(process_instance=instance).first()
 | 
				
			||||||
 | 
					    quote_items = list(quote.items.select_related('item').all()) if quote else []
 | 
				
			||||||
 | 
					    quote_price_map = {qi.item_id: qi.unit_price for qi in quote_items}
 | 
				
			||||||
 | 
					    items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
 | 
				
			||||||
 | 
					    manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Initialize the form
 | 
				
			||||||
 | 
					    form = None
 | 
				
			||||||
 | 
					    if request.method == 'POST' and request.POST.get('action') not in ['approve', 'reject']:
 | 
				
			||||||
 | 
					        # Handle form submission for report creation/editing
 | 
				
			||||||
 | 
					        if not user_is_installer:
 | 
				
			||||||
 | 
					            messages.error(request, 'شما مجوز ثبت/ویرایش گزارش نصب را ندارید')
 | 
				
			||||||
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					        # Block editing approved reports
 | 
				
			||||||
 | 
					        if existing_report and existing_report.approved:
 | 
				
			||||||
 | 
					            messages.error(request, 'این گزارش قبلا تایید شده و قابل ویرایش نیست')
 | 
				
			||||||
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        form = InstallationReportForm(
 | 
				
			||||||
 | 
					            request.POST, 
 | 
				
			||||||
 | 
					            instance=existing_report if edit_mode else None,
 | 
				
			||||||
 | 
					            user_is_installer=user_is_installer,
 | 
				
			||||||
 | 
					            instance_well=instance.well
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if form.is_valid():
 | 
				
			||||||
 | 
					            # Validate photos
 | 
				
			||||||
 | 
					            photo_validation_passed = False
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                deleted_photo_ids = []
 | 
				
			||||||
 | 
					                for key, val in request.POST.items():
 | 
				
			||||||
 | 
					                    if key.startswith('del_photo_') and val == '1':
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            pid = key.split('_')[-1]
 | 
				
			||||||
 | 
					                            deleted_photo_ids.append(pid)
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                existing_photos = existing_report.photos.all() if existing_report else None
 | 
				
			||||||
 | 
					                form.validate_photos(request.FILES, existing_photos, deleted_photo_ids)
 | 
				
			||||||
 | 
					                photo_validation_passed = True
 | 
				
			||||||
 | 
					            except ValidationError as e:
 | 
				
			||||||
 | 
					                form.add_error(None, str(e))
 | 
				
			||||||
 | 
					                # Re-render form with photo validation error
 | 
				
			||||||
 | 
					                photo_validation_passed = False
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Always clear approvals/rejections when form is submitted (even if photo validation fails)
 | 
				
			||||||
 | 
					            # Reset step status and clear approvals/rejections
 | 
				
			||||||
 | 
					            step_instance.status = 'in_progress'
 | 
				
			||||||
 | 
					            step_instance.completed_at = None
 | 
				
			||||||
 | 
					            step_instance.save()
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                for appr in list(step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                    appr.delete()
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                for rej in list(step_instance.rejections.all()):
 | 
				
			||||||
 | 
					                    rej.delete()
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Reopen subsequent steps
 | 
				
			||||||
 | 
					            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':
 | 
				
			||||||
 | 
					                        instance.step_instances.filter(step=subsequent_step).update(
 | 
				
			||||||
 | 
					                            status='in_progress',
 | 
				
			||||||
 | 
					                            completed_at=None
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            for appr in list(subsequent_step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                                appr.delete()
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            pass
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Reset current step if needed
 | 
				
			||||||
 | 
					            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
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Only save the report if photo validation passed
 | 
				
			||||||
 | 
					            if photo_validation_passed:
 | 
				
			||||||
 | 
					                # Save the form
 | 
				
			||||||
 | 
					                report = form.save(commit=False)
 | 
				
			||||||
 | 
					                if not existing_report:
 | 
				
			||||||
 | 
					                    report.assignment = assignment
 | 
				
			||||||
 | 
					                    report.created_by = request.user
 | 
				
			||||||
 | 
					                report.approved = False  # Reset approval status
 | 
				
			||||||
 | 
					                report.save()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Handle photo uploads and deletions
 | 
				
			||||||
 | 
					                if existing_report and edit_mode:
 | 
				
			||||||
 | 
					                    # Delete selected existing photos
 | 
				
			||||||
 | 
					                    for key, val in request.POST.items():
 | 
				
			||||||
 | 
					                        if key.startswith('del_photo_') and val == '1':
 | 
				
			||||||
 | 
					                            try:
 | 
				
			||||||
 | 
					                                pid = int(key.split('_')[-1])
 | 
				
			||||||
 | 
					                                InstallationPhoto.objects.filter(id=pid, report=report).delete()
 | 
				
			||||||
 | 
					                            except Exception:
 | 
				
			||||||
 | 
					                                continue
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Add new photos
 | 
				
			||||||
 | 
					                for f in request.FILES.getlist('photos'):
 | 
				
			||||||
 | 
					                    InstallationPhoto.objects.create(report=report, image=f)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Handle item changes (this logic remains the same)
 | 
				
			||||||
 | 
					                remove_map = {}
 | 
				
			||||||
 | 
					                add_map = {}
 | 
				
			||||||
 | 
					                for key in request.POST.keys():
 | 
				
			||||||
 | 
					                    if key.startswith('rem_') and key.endswith('_type'):
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            item_id = int(key.split('_')[1])
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                        if request.POST.get(key) != 'remove':
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                        qty_val = request.POST.get(f'rem_{item_id}_qty') or '1'
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            qty = int(qty_val)
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            qty = 1
 | 
				
			||||||
 | 
					                        remove_map[item_id] = qty
 | 
				
			||||||
 | 
					                    if key.startswith('add_') and key.endswith('_type'):
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            item_id = int(key.split('_')[1])
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                        if request.POST.get(key) != 'add':
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                        qty_val = request.POST.get(f'add_{item_id}_qty') or '1'
 | 
				
			||||||
 | 
					                        price_val = request.POST.get(f'add_{item_id}_price')
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            qty = int(qty_val)
 | 
				
			||||||
 | 
					                        except Exception:
 | 
				
			||||||
 | 
					                            qty = 1
 | 
				
			||||||
 | 
					                        # resolve unit price
 | 
				
			||||||
 | 
					                        unit_price = None
 | 
				
			||||||
 | 
					                        if price_val:
 | 
				
			||||||
 | 
					                            try:
 | 
				
			||||||
 | 
					                                unit_price = Decimal(price_val)
 | 
				
			||||||
 | 
					                            except InvalidOperation:
 | 
				
			||||||
 | 
					                                unit_price = None
 | 
				
			||||||
 | 
					                        if unit_price is None:
 | 
				
			||||||
 | 
					                            item_obj = Item.objects.filter(id=item_id).first()
 | 
				
			||||||
 | 
					                            unit_price = item_obj.unit_price if item_obj else None
 | 
				
			||||||
 | 
					                        add_map[item_id] = {'qty': qty, 'price': unit_price}
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Replace item changes with new submission
 | 
				
			||||||
 | 
					                if existing_report and edit_mode:
 | 
				
			||||||
 | 
					                    report.item_changes.all().delete()
 | 
				
			||||||
 | 
					                create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.')
 | 
				
			||||||
 | 
					                return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # GET request or approval/rejection actions - initialize form for display
 | 
				
			||||||
 | 
					        form = InstallationReportForm(
 | 
				
			||||||
 | 
					            instance=existing_report if existing_report else None,
 | 
				
			||||||
 | 
					            user_is_installer=user_is_installer,
 | 
				
			||||||
 | 
					            instance_well=instance.well
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Build approver requirements/status for UI
 | 
					    # Build approver requirements/status for UI
 | 
				
			||||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
					    reqs = list(step.approver_requirements.select_related('role').all())
 | 
				
			||||||
    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
					    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
				
			||||||
| 
						 | 
					@ -148,7 +319,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        can_approve_reject = False
 | 
					        can_approve_reject = False
 | 
				
			||||||
    user_can_approve = can_approve_reject
 | 
					    user_can_approve = can_approve_reject
 | 
				
			||||||
    approvals_list = list(step_instance.approvals.select_related('role').all())
 | 
					    approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
 | 
				
			||||||
    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
					    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
				
			||||||
    approver_statuses = [
 | 
					    approver_statuses = [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					@ -159,6 +330,15 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
        for r in reqs
 | 
					        for r in reqs
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Determine if current user has already approved/rejected (to disable buttons)
 | 
				
			||||||
 | 
					    current_user_has_decided = False
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        user_has_approval = step_instance.approvals.filter(approved_by=request.user, is_deleted=False).exists()
 | 
				
			||||||
 | 
					        user_has_rejection = step_instance.rejections.filter(rejected_by=request.user, is_deleted=False).exists()
 | 
				
			||||||
 | 
					        current_user_has_decided = bool(user_has_approval or user_has_rejection)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        current_user_has_decided = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Manager approval/rejection actions
 | 
					    # Manager approval/rejection actions
 | 
				
			||||||
    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
					    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
				
			||||||
        action = request.POST.get('action')
 | 
					        action = request.POST.get('action')
 | 
				
			||||||
| 
						 | 
					@ -175,14 +355,16 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if action == 'approve':
 | 
					        if action == 'approve':
 | 
				
			||||||
            existing_report.approved = True
 | 
					            # Record this user's approval for their role
 | 
				
			||||||
            existing_report.save()
 | 
					 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepApproval.objects.update_or_create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
					                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            # Only mark report approved when ALL required roles have approved
 | 
				
			||||||
            if step_instance.is_fully_approved():
 | 
					            if step_instance.is_fully_approved():
 | 
				
			||||||
 | 
					                existing_report.approved = True
 | 
				
			||||||
 | 
					                existing_report.save()
 | 
				
			||||||
                step_instance.status = 'completed'
 | 
					                step_instance.status = 'completed'
 | 
				
			||||||
                step_instance.completed_at = timezone.now()
 | 
					                step_instance.completed_at = timezone.now()
 | 
				
			||||||
                step_instance.save()
 | 
					                step_instance.save()
 | 
				
			||||||
| 
						 | 
					@ -191,6 +373,11 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
                    instance.save()
 | 
					                    instance.save()
 | 
				
			||||||
                    return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
 | 
					                    return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
 | 
				
			||||||
                return redirect('processes:request_list')
 | 
					                return redirect('processes:request_list')
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # Not fully approved yet; keep report as not approved
 | 
				
			||||||
 | 
					                if existing_report.approved:
 | 
				
			||||||
 | 
					                    existing_report.approved = False
 | 
				
			||||||
 | 
					                    existing_report.save(update_fields=['approved'])
 | 
				
			||||||
            messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
 | 
					            messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقشها.')
 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -217,160 +404,6 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
            messages.success(request, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.')
 | 
					            messages.success(request, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.')
 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
					            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if request.method == 'POST':
 | 
					 | 
				
			||||||
        # Only installers can submit or edit reports (non-approval actions)
 | 
					 | 
				
			||||||
        if request.POST.get('action') not in ['approve', 'reject'] and not user_is_installer:
 | 
					 | 
				
			||||||
            messages.error(request, 'شما مجوز ثبت/ویرایش گزارش نصب را ندارید')
 | 
					 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
 | 
					 | 
				
			||||||
        description = (request.POST.get('description') or '').strip()
 | 
					 | 
				
			||||||
        visited_date = (request.POST.get('visited_date') or '').strip()
 | 
					 | 
				
			||||||
        if '/' in visited_date:
 | 
					 | 
				
			||||||
            visited_date = visited_date.replace('/', '-')
 | 
					 | 
				
			||||||
        new_serial = (request.POST.get('new_water_meter_serial') or '').strip()
 | 
					 | 
				
			||||||
        seal_number = (request.POST.get('seal_number') or '').strip()
 | 
					 | 
				
			||||||
        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 = {}
 | 
					 | 
				
			||||||
        add_map = {}
 | 
					 | 
				
			||||||
        for key in request.POST.keys():
 | 
					 | 
				
			||||||
            if key.startswith('rem_') and key.endswith('_type'):
 | 
					 | 
				
			||||||
                # rem_{id}_type = 'remove'
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    item_id = int(key.split('_')[1])
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                if request.POST.get(key) != 'remove':
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                qty_val = request.POST.get(f'rem_{item_id}_qty') or '1'
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    qty = int(qty_val)
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    qty = 1
 | 
					 | 
				
			||||||
                remove_map[item_id] = qty
 | 
					 | 
				
			||||||
            if key.startswith('add_') and key.endswith('_type'):
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    item_id = int(key.split('_')[1])
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                if request.POST.get(key) != 'add':
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                qty_val = request.POST.get(f'add_{item_id}_qty') or '1'
 | 
					 | 
				
			||||||
                price_val = request.POST.get(f'add_{item_id}_price')
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    qty = int(qty_val)
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    qty = 1
 | 
					 | 
				
			||||||
                # resolve unit price
 | 
					 | 
				
			||||||
                unit_price = None
 | 
					 | 
				
			||||||
                if price_val:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        unit_price = Decimal(price_val)
 | 
					 | 
				
			||||||
                    except InvalidOperation:
 | 
					 | 
				
			||||||
                        unit_price = None
 | 
					 | 
				
			||||||
                if unit_price is None:
 | 
					 | 
				
			||||||
                    item_obj = Item.objects.filter(id=item_id).first()
 | 
					 | 
				
			||||||
                    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
 | 
					 | 
				
			||||||
            report.visited_date = visited_date or None
 | 
					 | 
				
			||||||
            report.new_water_meter_serial = new_serial or None
 | 
					 | 
				
			||||||
            report.seal_number = seal_number or None
 | 
					 | 
				
			||||||
            report.is_meter_suspicious = is_suspicious
 | 
					 | 
				
			||||||
            report.utm_x = utm_x
 | 
					 | 
				
			||||||
            report.utm_y = utm_y
 | 
					 | 
				
			||||||
            report.approved = False  # back to awaiting approval after edits
 | 
					 | 
				
			||||||
            report.save()
 | 
					 | 
				
			||||||
            # delete selected existing photos
 | 
					 | 
				
			||||||
            for key, val in request.POST.items():
 | 
					 | 
				
			||||||
                if key.startswith('del_photo_') and val == '1':
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        pid = int(key.split('_')[-1])
 | 
					 | 
				
			||||||
                        InstallationPhoto.objects.filter(id=pid, report=report).delete()
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        continue
 | 
					 | 
				
			||||||
            # append new photos
 | 
					 | 
				
			||||||
            for f in request.FILES.getlist('photos'):
 | 
					 | 
				
			||||||
                InstallationPhoto.objects.create(report=report, image=f)
 | 
					 | 
				
			||||||
            # replace item changes with new submission
 | 
					 | 
				
			||||||
            report.item_changes.all().delete()
 | 
					 | 
				
			||||||
            create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            report = InstallationReport.objects.create(
 | 
					 | 
				
			||||||
                assignment=assignment,
 | 
					 | 
				
			||||||
                description=description,
 | 
					 | 
				
			||||||
                visited_date=visited_date or None,
 | 
					 | 
				
			||||||
                new_water_meter_serial=new_serial or None,
 | 
					 | 
				
			||||||
                seal_number=seal_number or None,
 | 
					 | 
				
			||||||
                is_meter_suspicious=is_suspicious,
 | 
					 | 
				
			||||||
                utm_x=utm_x,
 | 
					 | 
				
			||||||
                utm_y=utm_y,
 | 
					 | 
				
			||||||
                created_by=request.user,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            # photos
 | 
					 | 
				
			||||||
            for f in request.FILES.getlist('photos'):
 | 
					 | 
				
			||||||
                InstallationPhoto.objects.create(report=report, image=f)
 | 
					 | 
				
			||||||
            # item changes
 | 
					 | 
				
			||||||
            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'
 | 
					 | 
				
			||||||
        step_instance.completed_at = None
 | 
					 | 
				
			||||||
        step_instance.save()
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            step_instance.approvals.all().delete()
 | 
					 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Build prefill maps from existing report changes
 | 
					    # Build prefill maps from existing report changes
 | 
				
			||||||
    removed_ids = set()
 | 
					    removed_ids = set()
 | 
				
			||||||
| 
						 | 
					@ -389,11 +422,13 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
        'step': step,
 | 
					        'step': step,
 | 
				
			||||||
        'assignment': assignment,
 | 
					        'assignment': assignment,
 | 
				
			||||||
        'report': existing_report,
 | 
					        'report': existing_report,
 | 
				
			||||||
 | 
					        'form': form,
 | 
				
			||||||
        'edit_mode': edit_mode,
 | 
					        'edit_mode': edit_mode,
 | 
				
			||||||
        'user_is_installer': user_is_installer,
 | 
					        'user_is_installer': user_is_installer,
 | 
				
			||||||
        'quote': quote,
 | 
					        'quote': quote,
 | 
				
			||||||
        'quote_items': quote_items,
 | 
					        'quote_items': quote_items,
 | 
				
			||||||
        'all_items': items,
 | 
					        'all_items': items,
 | 
				
			||||||
 | 
					        'manufacturers': manufacturers,
 | 
				
			||||||
        'removed_ids': removed_ids,
 | 
					        'removed_ids': removed_ids,
 | 
				
			||||||
        'removed_qty': removed_qty,
 | 
					        'removed_qty': removed_qty,
 | 
				
			||||||
        'added_map': added_map,
 | 
					        'added_map': added_map,
 | 
				
			||||||
| 
						 | 
					@ -403,6 +438,7 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
        'approver_statuses': approver_statuses,
 | 
					        'approver_statuses': approver_statuses,
 | 
				
			||||||
        'user_can_approve': user_can_approve,
 | 
					        'user_can_approve': user_can_approve,
 | 
				
			||||||
        'can_approve_reject': can_approve_reject,
 | 
					        'can_approve_reject': can_approve_reject,
 | 
				
			||||||
 | 
					        'current_user_has_decided': current_user_has_decided,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,6 @@ class Quote(NameSlugModel):
 | 
				
			||||||
    def calculate_totals(self):
 | 
					    def calculate_totals(self):
 | 
				
			||||||
        """محاسبه مبالغ کل"""
 | 
					        """محاسبه مبالغ کل"""
 | 
				
			||||||
        total = sum(item.total_price for item in self.items.filter(is_deleted=False).all())
 | 
					        total = sum(item.total_price for item in self.items.filter(is_deleted=False).all())
 | 
				
			||||||
        total = sum(item.total_price for item in self.items.filter(is_deleted=False).all())
 | 
					 | 
				
			||||||
        self.total_amount = total
 | 
					        self.total_amount = total
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # محاسبه تخفیف
 | 
					        # محاسبه تخفیف
 | 
				
			||||||
| 
						 | 
					@ -115,7 +114,14 @@ class Quote(NameSlugModel):
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.discount_amount = 0
 | 
					            self.discount_amount = 0
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
					        # محاسبه مبلغ نهایی با احتساب مالیات
 | 
				
			||||||
 | 
					        base_amount = self.total_amount - self.discount_amount
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0)))
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            vat_rate = Decimal('0')
 | 
				
			||||||
 | 
					        vat_amount = base_amount * vat_rate
 | 
				
			||||||
 | 
					        self.final_amount = base_amount + vat_amount
 | 
				
			||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_status_display_with_color(self):
 | 
					    def get_status_display_with_color(self):
 | 
				
			||||||
| 
						 | 
					@ -263,7 +269,15 @@ class Invoice(NameSlugModel):
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.discount_amount = 0
 | 
					            self.discount_amount = 0
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
					        # محاسبه مبلغ نهایی با احتساب مالیات
 | 
				
			||||||
 | 
					        base_amount = self.total_amount - self.discount_amount
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            vat_rate = Decimal(str(getattr(settings, 'VAT_RATE', 0)))
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            vat_rate = Decimal('0')
 | 
				
			||||||
 | 
					        vat_amount = base_amount * vat_rate
 | 
				
			||||||
 | 
					        self.final_amount = base_amount + vat_amount
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        # خالص مانده به نفع شرکت (مثبت) یا به نفع مشتری (منفی)
 | 
					        # خالص مانده به نفع شرکت (مثبت) یا به نفع مشتری (منفی)
 | 
				
			||||||
        net_due = self.final_amount - self.paid_amount
 | 
					        net_due = self.final_amount - self.paid_amount
 | 
				
			||||||
        self.remaining_amount = net_due
 | 
					        self.remaining_amount = net_due
 | 
				
			||||||
| 
						 | 
					@ -280,6 +294,7 @@ class Invoice(NameSlugModel):
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_status_display_with_color(self):
 | 
					    def get_status_display_with_color(self):
 | 
				
			||||||
        """نمایش وضعیت با رنگ"""
 | 
					        """نمایش وضعیت با رنگ"""
 | 
				
			||||||
        status_colors = {
 | 
					        status_colors = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,11 @@
 | 
				
			||||||
    <!-- Customer & Well Info -->
 | 
					    <!-- Customer & Well Info -->
 | 
				
			||||||
    <div class="row mb-3">
 | 
					    <div class="row mb-3">
 | 
				
			||||||
      <div class="col-6">
 | 
					      <div class="col-6">
 | 
				
			||||||
        <h6 class="fw-bold mb-2">اطلاعات مشترک</h6>
 | 
					        <h6 class="fw-bold mb-2">اطلاعات مشترک {% if instance.representative.profile and instance.representative.profile.user_type == 'legal' %}(حقوقی){% else %}(حقیقی){% endif %}</h6>
 | 
				
			||||||
 | 
					        {% if instance.representative.profile and instance.representative.profile.user_type == 'legal' %}
 | 
				
			||||||
 | 
					        <div class="small mb-1"><span class="text-muted">نام شرکت:</span> {{ instance.representative.profile.company_name|default:"-" }}</div>
 | 
				
			||||||
 | 
					        <div class="small mb-1"><span class="text-muted">شناسه ملی:</span> {{ instance.representative.profile.company_national_id|default:"-" }}</div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
        <div class="small mb-1"><span class="text-muted">نام:</span> {{ invoice.customer.get_full_name|default:instance.representative.get_full_name }}</div>
 | 
					        <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 %}
 | 
					        {% 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>
 | 
					        <div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
 | 
				
			||||||
| 
						 | 
					@ -150,7 +154,7 @@
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          {% endif %}
 | 
					          {% endif %}
 | 
				
			||||||
          <tr class="total-section border-top border-2">
 | 
					          <tr class="total-section border-top border-2">
 | 
				
			||||||
            <td colspan="5" class="text-end"><strong>مبلغ نهایی(تومان):</strong></td>
 | 
					            <td colspan="5" class="text-end"><strong>مبلغ نهایی (شامل مالیات)(تومان):</strong></td>
 | 
				
			||||||
            <td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
					            <td><strong>{{ invoice.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          <tr class="total-section">
 | 
					          <tr class="total-section">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@
 | 
				
			||||||
          <div class="row g-3 mb-3">
 | 
					          <div class="row g-3 mb-3">
 | 
				
			||||||
            <div class="col-6 col-md-3">
 | 
					            <div class="col-6 col-md-3">
 | 
				
			||||||
              <div class="border rounded p-3 h-100">
 | 
					              <div class="border rounded p-3 h-100">
 | 
				
			||||||
                <div class="small text-muted">مبلغ نهایی</div>
 | 
					                <div class="small text-muted">مبلغ نهایی (با مالیات)</div>
 | 
				
			||||||
                <div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                <div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,7 @@
 | 
				
			||||||
              </thead>
 | 
					              </thead>
 | 
				
			||||||
              <tbody>
 | 
					              <tbody>
 | 
				
			||||||
                {% for r in rows %}
 | 
					                {% for r in rows %}
 | 
				
			||||||
                <tr>
 | 
					                <tr class="{% if r.is_removed %}table-light text-muted{% endif %}">
 | 
				
			||||||
                  <td>
 | 
					                  <td>
 | 
				
			||||||
                    <div class="d-flex flex-column">
 | 
					                    <div class="d-flex flex-column">
 | 
				
			||||||
                      <span class="fw-semibold">{{ r.item.name }}</span>
 | 
					                      <span class="fw-semibold">{{ r.item.name }}</span>
 | 
				
			||||||
| 
						 | 
					@ -118,7 +118,13 @@
 | 
				
			||||||
                  <td class="text-center text-danger">{{ r.removed_qty }}</td>
 | 
					                  <td class="text-center text-danger">{{ r.removed_qty }}</td>
 | 
				
			||||||
                  <td class="text-center">{{ r.quantity }}</td>
 | 
					                  <td class="text-center">{{ r.quantity }}</td>
 | 
				
			||||||
                  <td class="text-end">{{ r.unit_price|floatformat:0|intcomma:False }}</td>
 | 
					                  <td class="text-end">{{ r.unit_price|floatformat:0|intcomma:False }}</td>
 | 
				
			||||||
                  <td class="text-end">{{ r.total_price|floatformat:0|intcomma:False }}</td>
 | 
					                  <td class="text-end">
 | 
				
			||||||
 | 
					                    {% if r.is_removed %}
 | 
				
			||||||
 | 
					                      <span class="text-muted">-</span>
 | 
				
			||||||
 | 
					                    {% else %}
 | 
				
			||||||
 | 
					                      {{ r.total_price|floatformat:0|intcomma:False }}
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
                {% empty %}
 | 
					                {% empty %}
 | 
				
			||||||
                <tr><td colspan="7" class="text-center text-muted">آیتمی یافت نشد</td></tr>
 | 
					                <tr><td colspan="7" class="text-center text-muted">آیتمی یافت نشد</td></tr>
 | 
				
			||||||
| 
						 | 
					@ -154,7 +160,7 @@
 | 
				
			||||||
                  <th class="text-end">{{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
					                  <th class="text-end">{{ invoice.discount_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
                <tr>
 | 
					                <tr>
 | 
				
			||||||
                  <th colspan="6" class="text-end">مبلغ نهایی</th>
 | 
					                  <th colspan="6" class="text-end">مبلغ نهایی (با مالیات)</th>
 | 
				
			||||||
                  <th class="text-end">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
					                  <th class="text-end">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
                <tr>
 | 
					                <tr>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,11 @@
 | 
				
			||||||
          <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
					          <a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
				
			||||||
            <i class="bx bx-printer me-2"></i> پرینت
 | 
					            <i class="bx bx-printer me-2"></i> پرینت
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
 | 
					          {% if request.user|is_manager and step_instance.status != 'approved' and step_instance.status != 'completed' and invoice.remaining_amount != 0 %}
 | 
				
			||||||
 | 
					          <button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#forceApproveModal">
 | 
				
			||||||
 | 
					            <i class="bx bx-bolt-circle me-1"></i> تایید اضطراری
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
					          <a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
 | 
				
			||||||
            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
					            <i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
 | 
				
			||||||
| 
						 | 
					@ -106,13 +111,17 @@
 | 
				
			||||||
        <div class="col-12 {% if is_broker %}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 mb-3 border">
 | 
				
			||||||
            <div class="card-header d-flex justify-content-between">
 | 
					            <div class="card-header d-flex justify-content-between">
 | 
				
			||||||
                <h5 class="mb-0">وضعیت فاکتور</h5>
 | 
					                <h5 class="mb-0">وضعیت فاکتور
 | 
				
			||||||
 | 
					                  {% if step_instance.status == 'approved' %}
 | 
				
			||||||
 | 
					                    <span class="badge bg-warning">تایید اضطراری</span>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                </h5>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="card-body">
 | 
					            <div class="card-body">
 | 
				
			||||||
              <div class="row g-3">
 | 
					              <div class="row g-3">
 | 
				
			||||||
                <div class="col-6 col-md-4">
 | 
					                <div class="col-6 col-md-4">
 | 
				
			||||||
                  <div class="border rounded p-3 h-100">
 | 
					                  <div class="border rounded p-3 h-100">
 | 
				
			||||||
                    <div class="small text-muted">مبلغ نهایی</div>
 | 
					                    <div class="small text-muted">مبلغ نهایی (با مالیات)</div>
 | 
				
			||||||
                    <div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                    <div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
| 
						 | 
					@ -189,10 +198,17 @@
 | 
				
			||||||
        <div class="card-header d-flex justify-content-between align-items-center">
 | 
					        <div class="card-header d-flex justify-content-between align-items-center">
 | 
				
			||||||
          <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
					          <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
				
			||||||
          {% if can_approve_reject %}
 | 
					          {% if can_approve_reject %}
 | 
				
			||||||
          <div class="d-flex gap-2">
 | 
					            {% if current_user_has_decided %}
 | 
				
			||||||
            <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal">تایید</button>
 | 
					            <div class="d-flex gap-2">
 | 
				
			||||||
            <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button>
 | 
					              <button type="button" class="btn btn-success btn-sm" disabled>تایید</button>
 | 
				
			||||||
          </div>
 | 
					              <button type="button" class="btn btn-danger btn-sm" disabled>رد</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {% else %}
 | 
				
			||||||
 | 
					              <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					                <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 %}
 | 
				
			||||||
          {% endif %}
 | 
					          {% endif %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="card-body py-3">
 | 
					        <div class="card-body py-3">
 | 
				
			||||||
| 
						 | 
					@ -243,6 +259,32 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- Force Approve Modal -->
 | 
				
			||||||
 | 
					<div class="modal fade" id="forceApproveModal" tabindex="-1" aria-hidden="true">
 | 
				
			||||||
 | 
					  <div class="modal-dialog">
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <form method="post">
 | 
				
			||||||
 | 
					        {% csrf_token %}
 | 
				
			||||||
 | 
					        <input type="hidden" name="action" value="force_approve">
 | 
				
			||||||
 | 
					        <div class="modal-header">
 | 
				
			||||||
 | 
					          <h5 class="modal-title">تایید اضطراری تسویه</h5>
 | 
				
			||||||
 | 
					          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="modal-body">
 | 
				
			||||||
 | 
					          <div class="alert alert-warning" role="alert">
 | 
				
			||||||
 | 
					          با تایید اضطراری ممکن است هنوز پرداخت کامل نشده باشد و این مرحله به صورت استثنا تایید میشود.
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          آیا از تایید اضطراری این مرحله اطمینان دارید؟
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="modal-footer">
 | 
				
			||||||
 | 
					          <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
 | 
					          <button type="submit" class="btn btn-warning">تایید اضطراری</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- Delete Confirmation Modal (final settlement payments) -->
 | 
					<!-- Delete Confirmation Modal (final settlement payments) -->
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +75,7 @@
 | 
				
			||||||
                        <input type="number" min="1" class="form-control" name="amount" id="id_amount" required>
 | 
					                        <input type="number" min="1" class="form-control" name="amount" id="id_amount" required>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <div class="mb-3">
 | 
					                      <div class="mb-3">
 | 
				
			||||||
                        <label class="form-label">تاریخ پرداخت</label>
 | 
					                        <label class="form-label">تاریخ پرداخت/سررسید چک</label>
 | 
				
			||||||
                        <input type="text" class="form-control" id="id_payment_date" name="payment_date" placeholder="انتخاب تاریخ" readonly required>
 | 
					                        <input type="text" class="form-control" id="id_payment_date" name="payment_date" placeholder="انتخاب تاریخ" readonly required>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <div class="mb-3">
 | 
					                      <div class="mb-3">
 | 
				
			||||||
| 
						 | 
					@ -89,7 +89,7 @@
 | 
				
			||||||
                        </select>
 | 
					                        </select>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <div class="mb-3">
 | 
					                      <div class="mb-3">
 | 
				
			||||||
                        <label class="form-label">شماره مرجع/چک</label>
 | 
					                        <label class="form-label">شماره پیگیری/شماره صیادی چک</label>
 | 
				
			||||||
                        <input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
 | 
					                        <input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <div class="mb-3">
 | 
					                      <div class="mb-3">
 | 
				
			||||||
| 
						 | 
					@ -116,7 +116,7 @@
 | 
				
			||||||
                      <div class="row g-3">
 | 
					                      <div class="row g-3">
 | 
				
			||||||
                        <div class="col-6">
 | 
					                        <div class="col-6">
 | 
				
			||||||
                          <div class="border rounded p-3">
 | 
					                          <div class="border rounded p-3">
 | 
				
			||||||
                            <div class="small text-muted">مبلغ نهایی پیشفاکتور</div>
 | 
					                            <div class="small text-muted">مبلغ نهایی پیشفاکتور (با مالیات)</div>
 | 
				
			||||||
                            <div class="h5 mt-1">{{ totals.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                            <div class="h5 mt-1">{{ totals.final_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
| 
						 | 
					@ -154,9 +154,9 @@
 | 
				
			||||||
                        <thead>
 | 
					                        <thead>
 | 
				
			||||||
                          <tr>
 | 
					                          <tr>
 | 
				
			||||||
                            <th>مبلغ</th>
 | 
					                            <th>مبلغ</th>
 | 
				
			||||||
                            <th>تاریخ</th>
 | 
					                            <th>تاریخ پرداخت/سررسید چک</th>
 | 
				
			||||||
                            <th>روش</th>
 | 
					                            <th>روش</th>
 | 
				
			||||||
                            <th>شماره مرجع/چک</th>
 | 
					                            <th>شماره پیگیری/شماره صیادی چک</th>
 | 
				
			||||||
                            <th>عملیات</th>
 | 
					                            <th>عملیات</th>
 | 
				
			||||||
                          </tr>
 | 
					                          </tr>
 | 
				
			||||||
                        </thead>
 | 
					                        </thead>
 | 
				
			||||||
| 
						 | 
					@ -197,10 +197,17 @@
 | 
				
			||||||
                    <div class="card-header d-flex justify-content-between align-items-center">
 | 
					                    <div class="card-header d-flex justify-content-between align-items-center">
 | 
				
			||||||
                      <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
					                      <h6 class="mb-0">وضعیت تاییدها</h6>
 | 
				
			||||||
                      {% if can_approve_reject %}
 | 
					                      {% if can_approve_reject %}
 | 
				
			||||||
                      <div class="d-flex gap-2">
 | 
					                        {% if current_user_has_decided %}
 | 
				
			||||||
                        <button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approvePaymentsModal2">تایید</button>
 | 
					                        <div class="d-flex gap-2">
 | 
				
			||||||
                        <button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
 | 
					                          <button type="button" class="btn btn-success btn-sm" disabled>تایید</button>
 | 
				
			||||||
                      </div>
 | 
					                          <button type="button" class="btn btn-danger btn-sm" disabled>رد</button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        {% else %}
 | 
				
			||||||
 | 
					                        <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					                          <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 %}
 | 
				
			||||||
                      {% endif %}
 | 
					                      {% endif %}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div class="card-body py-3">
 | 
					                    <div class="card-body py-3">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -114,8 +114,23 @@
 | 
				
			||||||
              <div class="">
 | 
					              <div class="">
 | 
				
			||||||
                <div class="card-body p-3">
 | 
					                <div class="card-body p-3">
 | 
				
			||||||
                  <h6 class="card-title text-primary mb-2">
 | 
					                  <h6 class="card-title text-primary mb-2">
 | 
				
			||||||
                    <i class="bx bx-user me-1"></i>اطلاعات مشترک
 | 
					                    <i class="bx bx-user me-1"></i>
 | 
				
			||||||
 | 
					                    {% if instance.representative.profile.user_type == 'legal' %}
 | 
				
			||||||
 | 
					                      اطلاعات مشترک (حقوقی)
 | 
				
			||||||
 | 
					                    {% else %}
 | 
				
			||||||
 | 
					                      اطلاعات مشترک (حقیقی)
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
                  </h6>
 | 
					                  </h6>
 | 
				
			||||||
 | 
					                  {% if instance.representative.profile.user_type == 'legal' %}
 | 
				
			||||||
 | 
					                  <div class="d-flex gap-2 mb-1">
 | 
				
			||||||
 | 
					                    <span class="text-muted small">نام شرکت:</span>
 | 
				
			||||||
 | 
					                    <span class="fw-medium small">{{ instance.representative.profile.company_name|default:"-" }}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="d-flex gap-2 mb-1">
 | 
				
			||||||
 | 
					                    <span class="text-muted small">شناسه ملی:</span>
 | 
				
			||||||
 | 
					                    <span class="fw-medium small">{{ instance.representative.profile.company_national_id|default:"-" }}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
                  <div class="d-flex gap-2 mb-1">
 | 
					                  <div class="d-flex gap-2 mb-1">
 | 
				
			||||||
                    <span class="text-muted small">نام:</span>
 | 
					                    <span class="text-muted small">نام:</span>
 | 
				
			||||||
                    <span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
 | 
					                    <span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
 | 
				
			||||||
| 
						 | 
					@ -198,7 +213,7 @@
 | 
				
			||||||
                  {% if quote.discount_amount > 0 %}
 | 
					                  {% if quote.discount_amount > 0 %}
 | 
				
			||||||
                  <p class="mb-2">تخفیف:</p>
 | 
					                  <p class="mb-2">تخفیف:</p>
 | 
				
			||||||
                  {% endif %}
 | 
					                  {% endif %}
 | 
				
			||||||
                  <p class="mb-0 fw-bold">مبلغ نهایی:</p>
 | 
					                  <p class="mb-0 fw-bold">مبلغ نهایی (شامل مالیات):</p>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td class="px-4 py-5">
 | 
					                <td class="px-4 py-5">
 | 
				
			||||||
                  <p class="fw-medium mb-2">{{ quote.total_amount|floatformat:0|intcomma:False }} تومان</p>
 | 
					                  <p class="fw-medium mb-2">{{ quote.total_amount|floatformat:0|intcomma:False }} تومان</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -203,7 +203,7 @@
 | 
				
			||||||
                    </tr>
 | 
					                    </tr>
 | 
				
			||||||
                    {% endif %}
 | 
					                    {% endif %}
 | 
				
			||||||
                    <tr class="total-section border-top border-2">
 | 
					                    <tr class="total-section border-top border-2">
 | 
				
			||||||
                        <td colspan="5" class="text-end"><strong>مبلغ نهایی(تومان):</strong></td>
 | 
					                        <td colspan="5" class="text-end"><strong>مبلغ نهایی (با مالیات)(تومان):</strong></td>
 | 
				
			||||||
                        <td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
					                        <td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
				
			||||||
                    </tr>
 | 
					                    </tr>
 | 
				
			||||||
                </tfoot>
 | 
					                </tfoot>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ from django.contrib import messages
 | 
				
			||||||
from django.http import JsonResponse
 | 
					from django.http import JsonResponse
 | 
				
			||||||
from django.views.decorators.http import require_POST
 | 
					from django.views.decorators.http import require_POST
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from decimal import Decimal, InvalidOperation
 | 
					from decimal import Decimal, InvalidOperation
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
| 
						 | 
					@ -356,16 +357,16 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
					    reqs = list(step.approver_requirements.select_related('role').all())
 | 
				
			||||||
    user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
 | 
					    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_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_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
 | 
				
			||||||
    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
					    approvals_by_role = {a.role_id: a for a in approvals_list}
 | 
				
			||||||
    approver_statuses = [
 | 
					    approver_statuses = []
 | 
				
			||||||
        {
 | 
					    for r in reqs:
 | 
				
			||||||
 | 
					        appr = approvals_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        approver_statuses.append({
 | 
				
			||||||
            'role': r.role,
 | 
					            'role': r.role,
 | 
				
			||||||
            'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
 | 
					            'status': (appr.decision if appr else None),
 | 
				
			||||||
            'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
 | 
					            'reason': (appr.reason if appr else ''),
 | 
				
			||||||
        }
 | 
					        })
 | 
				
			||||||
        for r in reqs
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
					    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -374,6 +375,15 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
					        can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        can_approve_reject = False
 | 
					        can_approve_reject = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Compute whether current user has already decided (approved/rejected)
 | 
				
			||||||
 | 
					    current_user_has_decided = False
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        user_has_approval = step_instance.approvals.filter(approved_by=request.user, is_deleted=False).exists()
 | 
				
			||||||
 | 
					        user_has_rejection = step_instance.rejections.filter(rejected_by=request.user, is_deleted=False).exists()
 | 
				
			||||||
 | 
					        current_user_has_decided = bool(user_has_approval or user_has_rejection)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        current_user_has_decided = False
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Accountant/Admin approval and rejection via POST (multi-role)
 | 
					    # Accountant/Admin approval and rejection via POST (multi-role)
 | 
				
			||||||
| 
						 | 
					@ -452,6 +462,7 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
        'is_broker': is_broker,
 | 
					        'is_broker': is_broker,
 | 
				
			||||||
        'is_accountant': is_accountant,
 | 
					        'is_accountant': is_accountant,
 | 
				
			||||||
        'can_approve_reject': can_approve_reject,
 | 
					        'can_approve_reject': can_approve_reject,
 | 
				
			||||||
 | 
					        'current_user_has_decided': current_user_has_decided,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -537,7 +548,17 @@ def add_quote_payment(request, instance_id, step_id):
 | 
				
			||||||
        si.status = 'in_progress'
 | 
					        si.status = 'in_progress'
 | 
				
			||||||
        si.completed_at = None
 | 
					        si.completed_at = None
 | 
				
			||||||
        si.save()
 | 
					        si.save()
 | 
				
			||||||
        si.approvals.all().delete()
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for appr in list(si.approvals.all()):
 | 
				
			||||||
 | 
					            appr.delete()
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for rej in list(si.rejections.all()):
 | 
				
			||||||
 | 
					            rej.delete()
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -554,7 +575,8 @@ def add_quote_payment(request, instance_id, step_id):
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Clear previous approvals if the step requires re-approval
 | 
					                # Clear previous approvals if the step requires re-approval
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    subsequent_step_instance.approvals.all().delete()
 | 
					                    for appr in list(subsequent_step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                        appr.delete()
 | 
				
			||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
| 
						 | 
					@ -596,7 +618,7 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        # soft delete using project's BaseModel delete override
 | 
					        # soft delete using project's BaseModel delete override
 | 
				
			||||||
        payment.delete()
 | 
					        payment.hard_delete()
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
 | 
					        return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
 | 
				
			||||||
    # On delete, return to awaiting approval
 | 
					    # On delete, return to awaiting approval
 | 
				
			||||||
| 
						 | 
					@ -605,7 +627,10 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
        si.status = 'in_progress'
 | 
					        si.status = 'in_progress'
 | 
				
			||||||
        si.completed_at = None
 | 
					        si.completed_at = None
 | 
				
			||||||
        si.save()
 | 
					        si.save()
 | 
				
			||||||
        si.approvals.all().delete()
 | 
					        for appr in list(si.approvals.all()):
 | 
				
			||||||
 | 
					            appr.delete()
 | 
				
			||||||
 | 
					        for rej in list(si.rejections.all()):
 | 
				
			||||||
 | 
					            rej.delete()
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -622,7 +647,8 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Clear previous approvals if the step requires re-approval
 | 
					                # Clear previous approvals if the step requires re-approval
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    subsequent_step_instance.approvals.all().delete()
 | 
					                    for appr in list(subsequent_step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                        appr.delete()
 | 
				
			||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
| 
						 | 
					@ -707,16 +733,15 @@ def final_invoice_step(request, instance_id, step_id):
 | 
				
			||||||
                if ch.unit_price:
 | 
					                if ch.unit_price:
 | 
				
			||||||
                    row['base_price'] = _to_decimal(ch.unit_price)
 | 
					                    row['base_price'] = _to_decimal(ch.unit_price)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Compute final invoice lines
 | 
					    # Compute final invoice lines (include fully removed items for display)
 | 
				
			||||||
    rows = []
 | 
					    rows = []
 | 
				
			||||||
    total_amount = Decimal('0')
 | 
					    total_amount = Decimal('0')
 | 
				
			||||||
    for _, r in item_id_to_row.items():
 | 
					    for _, r in item_id_to_row.items():
 | 
				
			||||||
        final_qty = max(0, (r['base_qty'] + r['added_qty'] - r['removed_qty']))
 | 
					        final_qty = max(0, (r['base_qty'] + r['added_qty'] - r['removed_qty']))
 | 
				
			||||||
        if final_qty == 0:
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        unit_price_dec = _to_decimal(r['base_price'])
 | 
					        unit_price_dec = _to_decimal(r['base_price'])
 | 
				
			||||||
        line_total = Decimal(final_qty) * unit_price_dec
 | 
					        line_total = Decimal(final_qty) * unit_price_dec if final_qty > 0 else Decimal('0')
 | 
				
			||||||
        total_amount += line_total
 | 
					        if final_qty > 0:
 | 
				
			||||||
 | 
					            total_amount += line_total
 | 
				
			||||||
        rows.append({
 | 
					        rows.append({
 | 
				
			||||||
            'item': r['item'],
 | 
					            'item': r['item'],
 | 
				
			||||||
            'quantity': final_qty,
 | 
					            'quantity': final_qty,
 | 
				
			||||||
| 
						 | 
					@ -725,6 +750,7 @@ def final_invoice_step(request, instance_id, step_id):
 | 
				
			||||||
            'base_qty': r['base_qty'],
 | 
					            'base_qty': r['base_qty'],
 | 
				
			||||||
            'added_qty': r['added_qty'],
 | 
					            'added_qty': r['added_qty'],
 | 
				
			||||||
            'removed_qty': r['removed_qty'],
 | 
					            'removed_qty': r['removed_qty'],
 | 
				
			||||||
 | 
					            'is_removed': True if final_qty == 0 else False,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create or reuse final invoice
 | 
					    # Create or reuse final invoice
 | 
				
			||||||
| 
						 | 
					@ -745,6 +771,8 @@ def final_invoice_step(request, instance_id, step_id):
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        qs.delete()
 | 
					        qs.delete()
 | 
				
			||||||
    for r in rows:
 | 
					    for r in rows:
 | 
				
			||||||
 | 
					        if r['quantity'] <= 0:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
        from .models import InvoiceItem
 | 
					        from .models import InvoiceItem
 | 
				
			||||||
        InvoiceItem.objects.create(
 | 
					        InvoiceItem.objects.create(
 | 
				
			||||||
            invoice=invoice,
 | 
					            invoice=invoice,
 | 
				
			||||||
| 
						 | 
					@ -918,12 +946,21 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        can_approve_reject = False
 | 
					        can_approve_reject = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Compute whether current user has already decided (approved/rejected)
 | 
				
			||||||
 | 
					    current_user_has_decided = False
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        user_has_approval = step_instance.approvals.filter(approved_by=request.user).exists()
 | 
				
			||||||
 | 
					        user_has_rejection = step_instance.rejections.filter(rejected_by=request.user).exists()
 | 
				
			||||||
 | 
					        current_user_has_decided = bool(user_has_approval or user_has_rejection)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        current_user_has_decided = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Accountant/Admin approval and rejection (multi-role)
 | 
					    # Accountant/Admin approval and rejection (multi-role)
 | 
				
			||||||
    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
 | 
					    if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject', 'force_approve']:
 | 
				
			||||||
        req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
 | 
					        req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
 | 
				
			||||||
        user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
 | 
					        user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
 | 
				
			||||||
        matching_role = next((r for r in user_roles if r in req_roles), None)
 | 
					        matching_role = next((r for r in user_roles if r in req_roles), None)
 | 
				
			||||||
        if matching_role is None:
 | 
					        if matching_role is None and request.POST.get('action') != 'force_approve':
 | 
				
			||||||
            messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
 | 
					            messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.')
 | 
				
			||||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
					            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -972,6 +1009,24 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
            messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
 | 
					            messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.')
 | 
				
			||||||
            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
					            return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if action == 'force_approve':
 | 
				
			||||||
 | 
					            # Only MANAGER can force approve
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)):
 | 
				
			||||||
 | 
					                    messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.')
 | 
				
			||||||
 | 
					                    return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                messages.error(request, 'فقط مدیر مجاز به تایید اضطراری است.')
 | 
				
			||||||
 | 
					                return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
 | 
					            # Mark step completed regardless of remaining amount/approvals
 | 
				
			||||||
 | 
					            step_instance.status = 'approved'
 | 
				
			||||||
 | 
					            step_instance.save()
 | 
				
			||||||
 | 
					            if next_step:
 | 
				
			||||||
 | 
					                instance.current_step = next_step
 | 
				
			||||||
 | 
					                instance.save()
 | 
				
			||||||
 | 
					                return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
 | 
				
			||||||
 | 
					            return redirect('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # broker flag for payment management permission
 | 
					    # broker flag for payment management permission
 | 
				
			||||||
    profile = getattr(request.user, 'profile', None)
 | 
					    profile = getattr(request.user, 'profile', None)
 | 
				
			||||||
    is_broker = False
 | 
					    is_broker = False
 | 
				
			||||||
| 
						 | 
					@ -991,6 +1046,8 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
        'approver_statuses': approver_statuses,
 | 
					        'approver_statuses': approver_statuses,
 | 
				
			||||||
        'can_approve_reject': can_approve_reject,
 | 
					        'can_approve_reject': can_approve_reject,
 | 
				
			||||||
        'is_broker': is_broker,
 | 
					        'is_broker': is_broker,
 | 
				
			||||||
 | 
					        'current_user_has_decided': current_user_has_decided,
 | 
				
			||||||
 | 
					        'is_manager': bool(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).filter(slug=UserRoles.MANAGER.value).exists()) if getattr(request.user, 'profile', None) else False,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1065,10 +1122,20 @@ def add_final_payment(request, instance_id, step_id):
 | 
				
			||||||
    # On delete, return to awaiting approval
 | 
					    # On delete, return to awaiting approval
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
					        si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
 | 
				
			||||||
        si.status = 'in_progress'
 | 
					        if si.status != 'approved':
 | 
				
			||||||
 | 
					            si.status = 'in_progress'
 | 
				
			||||||
        si.completed_at = None
 | 
					        si.completed_at = None
 | 
				
			||||||
        si.save()
 | 
					        si.save()
 | 
				
			||||||
        si.approvals.all().delete()
 | 
					        try:
 | 
				
			||||||
 | 
					            for appr in list(si.approvals.all()):
 | 
				
			||||||
 | 
					                appr.delete()
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for rej in list(si.rejections.all()):
 | 
				
			||||||
 | 
					                rej.delete()
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -1085,7 +1152,8 @@ def add_final_payment(request, instance_id, step_id):
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Clear previous approvals if the step requires re-approval
 | 
					                # Clear previous approvals if the step requires re-approval
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    subsequent_step_instance.approvals.all().delete()
 | 
					                    for appr in list(subsequent_step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                        appr.delete()
 | 
				
			||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
| 
						 | 
					@ -1124,7 +1192,7 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
            return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
					            return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
					        return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403)
 | 
				
			||||||
    payment.delete()
 | 
					    payment.hard_delete()
 | 
				
			||||||
    invoice.refresh_from_db()
 | 
					    invoice.refresh_from_db()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # On delete, return to awaiting approval
 | 
					    # On delete, return to awaiting approval
 | 
				
			||||||
| 
						 | 
					@ -1133,7 +1201,16 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
        si.status = 'in_progress'
 | 
					        si.status = 'in_progress'
 | 
				
			||||||
        si.completed_at = None
 | 
					        si.completed_at = None
 | 
				
			||||||
        si.save()
 | 
					        si.save()
 | 
				
			||||||
        si.approvals.all().delete()
 | 
					        try:
 | 
				
			||||||
 | 
					            for appr in list(si.approvals.all()):
 | 
				
			||||||
 | 
					                appr.delete()
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for rej in list(si.rejections.all()):
 | 
				
			||||||
 | 
					                rej.delete()
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1150,7 +1227,8 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Clear previous approvals if the step requires re-approval
 | 
					                # Clear previous approvals if the step requires re-approval
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    subsequent_step_instance.approvals.all().delete()
 | 
					                    for appr in list(subsequent_step_instance.approvals.all()):
 | 
				
			||||||
 | 
					                        appr.delete()
 | 
				
			||||||
                except Exception:
 | 
					                except Exception:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								locations/migrations/0004_alter_county_city.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								locations/migrations/0004_alter_county_city.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-21 07:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('locations', '0003_remove_broker_company'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='county',
 | 
				
			||||||
 | 
					            name='city',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.city', verbose_name='استان'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -162,7 +162,7 @@ class StepInstanceAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(StepRejection)
 | 
					@admin.register(StepRejection)
 | 
				
			||||||
class StepRejectionAdmin(SimpleHistoryAdmin):
 | 
					class StepRejectionAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
    list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at']
 | 
					    list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at', 'is_deleted']
 | 
				
			||||||
    list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
 | 
					    list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
 | 
				
			||||||
    search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason']
 | 
					    search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason']
 | 
				
			||||||
    readonly_fields = ['created_at']
 | 
					    readonly_fields = ['created_at']
 | 
				
			||||||
| 
						 | 
					@ -182,6 +182,6 @@ class StepApproverRequirementAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(StepApproval)
 | 
					@admin.register(StepApproval)
 | 
				
			||||||
class StepApprovalAdmin(admin.ModelAdmin):
 | 
					class StepApprovalAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_display = ("step_instance", "role", "decision", "approved_by", "created_at")
 | 
					    list_display = ("step_instance", "role", "decision", "approved_by", "created_at", "is_deleted")
 | 
				
			||||||
    list_filter = ("decision", "role", "step_instance__step__process")
 | 
					    list_filter = ("decision", "role", "step_instance__step__process")
 | 
				
			||||||
    search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username")
 | 
					    search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-27 06:17
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('processes', '0003_historicalstepinstance_edit_count_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalsteprejection',
 | 
				
			||||||
 | 
					            name='is_deleted',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, verbose_name='حذف شده'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='stepapproval',
 | 
				
			||||||
 | 
					            name='is_deleted',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, verbose_name='حذف شده'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='steprejection',
 | 
				
			||||||
 | 
					            name='is_deleted',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, verbose_name='حذف شده'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-27 15:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('processes', '0004_historicalsteprejection_is_deleted_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalstepinstance',
 | 
				
			||||||
 | 
					            name='status',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح'), ('approved', 'تایید اضطراری')], default='pending', max_length=20, verbose_name='وضعیت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stepinstance',
 | 
				
			||||||
 | 
					            name='status',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح'), ('approved', 'تایید اضطراری')], default='pending', max_length=20, verbose_name='وضعیت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -290,6 +290,10 @@ class ProcessInstance(SluggedModel):
 | 
				
			||||||
        dependencies = step.get_dependencies()
 | 
					        dependencies = step.get_dependencies()
 | 
				
			||||||
        for dependency_id in dependencies:
 | 
					        for dependency_id in dependencies:
 | 
				
			||||||
            step_instance = self.step_instances.filter(step_id=dependency_id).first()
 | 
					            step_instance = self.step_instances.filter(step_id=dependency_id).first()
 | 
				
			||||||
 | 
					            if step_instance and step_instance.status == 'in_progress' and step_instance.step.order == 3 and step.order == 4:
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					            if step_instance and step_instance.status == 'approved' and step_instance.step.order == 8 and step.order == 9:
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
            if not step_instance or step_instance.status != 'completed':
 | 
					            if not step_instance or step_instance.status != 'completed':
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
| 
						 | 
					@ -320,6 +324,7 @@ class StepInstance(models.Model):
 | 
				
			||||||
            ('skipped', 'رد شده'),
 | 
					            ('skipped', 'رد شده'),
 | 
				
			||||||
            ('blocked', 'مسدود شده'),
 | 
					            ('blocked', 'مسدود شده'),
 | 
				
			||||||
            ('rejected', 'رد شده و نیاز به اصلاح'),
 | 
					            ('rejected', 'رد شده و نیاز به اصلاح'),
 | 
				
			||||||
 | 
					            ('approved', 'تایید اضطراری'),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        default='pending',
 | 
					        default='pending',
 | 
				
			||||||
        verbose_name="وضعیت"
 | 
					        verbose_name="وضعیت"
 | 
				
			||||||
| 
						 | 
					@ -417,6 +422,7 @@ class StepRejection(models.Model):
 | 
				
			||||||
        blank=True
 | 
					        blank=True
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
 | 
					    created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
 | 
				
			||||||
 | 
					    is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
 | 
				
			||||||
    history = HistoricalRecords()
 | 
					    history = HistoricalRecords()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
| 
						 | 
					@ -433,6 +439,14 @@ class StepRejection(models.Model):
 | 
				
			||||||
        self.step_instance.save()
 | 
					        self.step_instance.save()
 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def hard_delete(self):
 | 
				
			||||||
 | 
					        super().delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self.is_deleted = True
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StepApproverRequirement(models.Model):
 | 
					class StepApproverRequirement(models.Model):
 | 
				
			||||||
    """Required approver roles for a step."""
 | 
					    """Required approver roles for a step."""
 | 
				
			||||||
| 
						 | 
					@ -457,11 +471,20 @@ class StepApproval(models.Model):
 | 
				
			||||||
    decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه')
 | 
					    decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه')
 | 
				
			||||||
    reason = models.TextField(blank=True, verbose_name='علت (برای رد)')
 | 
					    reason = models.TextField(blank=True, verbose_name='علت (برای رد)')
 | 
				
			||||||
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
 | 
					    created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')
 | 
				
			||||||
 | 
					    is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        unique_together = ('step_instance', 'role')
 | 
					        unique_together = ('step_instance', 'role')
 | 
				
			||||||
        verbose_name = 'تایید مرحله'
 | 
					        verbose_name = 'تایید مرحله'
 | 
				
			||||||
        verbose_name_plural = 'تاییدهای مرحله'
 | 
					        verbose_name_plural = 'تاییدهای مرحله'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def delete(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self.is_deleted = True
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def hard_delete(self):
 | 
				
			||||||
 | 
					        super().delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"{self.step_instance} - {self.role} - {self.decision}"
 | 
					        return f"{self.step_instance} - {self.role} - {self.decision}"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
{% extends '_base.html' %}
 | 
					{% extends '_base.html' %}
 | 
				
			||||||
{% load static %}
 | 
					{% load static %}
 | 
				
			||||||
{% load accounts_tags %}
 | 
					{% load accounts_tags %}
 | 
				
			||||||
 | 
					{% load common_tags %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block sidebar %}
 | 
					{% block sidebar %}
 | 
				
			||||||
    {% include 'sidebars/admin.html' %}
 | 
					    {% include 'sidebars/admin.html' %}
 | 
				
			||||||
| 
						 | 
					@ -36,12 +37,10 @@
 | 
				
			||||||
    <div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
 | 
					    <div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
 | 
				
			||||||
      <div class="dt-buttons btn-group flex-wrap mb-0">
 | 
					      <div class="dt-buttons btn-group flex-wrap mb-0">
 | 
				
			||||||
        <div class="btn-group">
 | 
					        <div class="btn-group">
 | 
				
			||||||
          <button class="btn buttons-collection btn-label-primary dropdown-toggle me-4 d-none" type="button">
 | 
					          <button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()">
 | 
				
			||||||
            <span>
 | 
					            <span class="d-flex align-items-center gap-2">
 | 
				
			||||||
              <span class="d-flex align-items-center gap-2">
 | 
					              <i class="bx bx-export me-sm-1"></i>
 | 
				
			||||||
                <i class="icon-base bx bx-export me-sm-1"></i>
 | 
					              <span class="d-none d-sm-inline-block">خروجی اکسل</span>
 | 
				
			||||||
                <span class="d-none d-sm-inline-block">خروجی</span>
 | 
					 | 
				
			||||||
              </span>
 | 
					 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
          {% if request.user|is_broker %}
 | 
					          {% if request.user|is_broker %}
 | 
				
			||||||
| 
						 | 
					@ -212,6 +211,7 @@
 | 
				
			||||||
            <th>امور</th>
 | 
					            <th>امور</th>
 | 
				
			||||||
            <th>پیشرفت</th>
 | 
					            <th>پیشرفت</th>
 | 
				
			||||||
            <th>وضعیت</th>
 | 
					            <th>وضعیت</th>
 | 
				
			||||||
 | 
					            <th>تاریخ نصب/تاخیر</th>
 | 
				
			||||||
            <th>تاریخ ایجاد</th>
 | 
					            <th>تاریخ ایجاد</th>
 | 
				
			||||||
            <th>عملیات</th>
 | 
					            <th>عملیات</th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
| 
						 | 
					@ -244,6 +244,20 @@
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
            <td>{{ item.instance.get_status_display_with_color|safe }}</td>
 | 
					            <td>{{ item.instance.get_status_display_with_color|safe }}</td>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					              {% if item.installation_scheduled_date %}
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                  <span title="{{ item.installation_scheduled_date|date:'Y-m-d' }}">{{ item.installation_scheduled_date | to_jalali }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {% if item.installation_overdue_days and item.installation_overdue_days > 0 %}
 | 
				
			||||||
 | 
					                  <small class="text-danger d-block">{{ item.installation_overdue_days }} روز تاخیر</small>
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                  <small class="text-muted d-block">بدون تاخیر</small>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					              {% else %}
 | 
				
			||||||
 | 
					                <span class="text-muted">تاریخ نصب تعیین نشده</span>
 | 
				
			||||||
 | 
					              {% endif %}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
            <td>{{ item.instance.jcreated_date }}</td>
 | 
					            <td>{{ item.instance.jcreated_date }}</td>
 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
              <div class="d-inline-block">
 | 
					              <div class="d-inline-block">
 | 
				
			||||||
| 
						 | 
					@ -287,6 +301,7 @@
 | 
				
			||||||
            <td></td>
 | 
					            <td></td>
 | 
				
			||||||
            <td></td>
 | 
					            <td></td>
 | 
				
			||||||
            <td></td>
 | 
					            <td></td>
 | 
				
			||||||
 | 
					            <td></td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          {% endfor %}
 | 
					          {% endfor %}
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
| 
						 | 
					@ -419,6 +434,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div id="repNewFields" class="col-sm-12" style="display:none;">
 | 
					              <div id="repNewFields" class="col-sm-12" style="display:none;">
 | 
				
			||||||
                <div class="row g-3">
 | 
					                <div class="row g-3">
 | 
				
			||||||
 | 
					                  <div class="col-sm-12">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_user_type">{{ customer_form.user_type.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.user_type }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-sm-6">
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
                    <label class="form-label" for="id_first_name">{{ customer_form.first_name.label }}</label>
 | 
					                    <label class="form-label" for="id_first_name">{{ customer_form.first_name.label }}</label>
 | 
				
			||||||
                    {{ customer_form.first_name }}
 | 
					                    {{ customer_form.first_name }}
 | 
				
			||||||
| 
						 | 
					@ -439,6 +458,15 @@
 | 
				
			||||||
                    <label class="form-label" for="id_national_code">{{ customer_form.national_code.label }}</label>
 | 
					                    <label class="form-label" for="id_national_code">{{ customer_form.national_code.label }}</label>
 | 
				
			||||||
                    {{ customer_form.national_code }}
 | 
					                    {{ customer_form.national_code }}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <!-- Company fields for legal entities -->
 | 
				
			||||||
 | 
					                  <div class="col-sm-6 company-fields" style="display:none;">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_company_name">{{ customer_form.company_name.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.company_name }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-sm-6 company-fields" style="display:none;">
 | 
				
			||||||
 | 
					                    <label class="form-label" for="id_company_national_id">{{ customer_form.company_national_id.label }}</label>
 | 
				
			||||||
 | 
					                    {{ customer_form.company_national_id }}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-sm-6">
 | 
					                  <div class="col-sm-6">
 | 
				
			||||||
                    <label class="form-label" for="id_card_number">{{ customer_form.card_number.label }}</label>
 | 
					                    <label class="form-label" for="id_card_number">{{ customer_form.card_number.label }}</label>
 | 
				
			||||||
                    {{ customer_form.card_number }}
 | 
					                    {{ customer_form.card_number }}
 | 
				
			||||||
| 
						 | 
					@ -717,6 +745,21 @@
 | 
				
			||||||
        .fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
 | 
					        .fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function toggleRepCompanyFields() {
 | 
				
			||||||
 | 
					      const userType = $('#user-type-select').val();
 | 
				
			||||||
 | 
					      if (userType === 'legal') {
 | 
				
			||||||
 | 
					        $('#repNewFields .company-fields').show();
 | 
				
			||||||
 | 
					        $('input[name="company_name"]').attr('required', true);
 | 
				
			||||||
 | 
					        $('input[name="company_national_id"]').attr('required', true);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        $('#repNewFields .company-fields').hide();
 | 
				
			||||||
 | 
					        $('input[name="company_name"]').removeAttr('required');
 | 
				
			||||||
 | 
					        $('input[name="company_national_id"]').removeAttr('required');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#user-type-select').on('change', toggleRepCompanyFields);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#btnLookupRep').on('click', function() {
 | 
					    $('#btnLookupRep').on('click', function() {
 | 
				
			||||||
      const nc = $('#rep_national_code').val().trim();
 | 
					      const nc = $('#rep_national_code').val().trim();
 | 
				
			||||||
      if (!nc) { setStatus('#repStatus', 'لطفا کد ملی نماینده را وارد کنید', 'danger'); return; }
 | 
					      if (!nc) { setStatus('#repStatus', 'لطفا کد ملی نماینده را وارد کنید', 'danger'); return; }
 | 
				
			||||||
| 
						 | 
					@ -732,36 +775,47 @@
 | 
				
			||||||
            $('#id_first_name').val(resp.user.first_name || '');
 | 
					            $('#id_first_name').val(resp.user.first_name || '');
 | 
				
			||||||
            $('#id_last_name').val(resp.user.last_name || '');
 | 
					            $('#id_last_name').val(resp.user.last_name || '');
 | 
				
			||||||
            if (resp.user.profile) {
 | 
					            if (resp.user.profile) {
 | 
				
			||||||
 | 
					              $('#user-type-select').val(resp.user.profile.user_type || 'individual');
 | 
				
			||||||
              $('#id_national_code').val(resp.user.profile.national_code || nc);
 | 
					              $('#id_national_code').val(resp.user.profile.national_code || nc);
 | 
				
			||||||
              $('#id_phone_number_1').val(resp.user.profile.phone_number_1 || '');
 | 
					              $('#id_phone_number_1').val(resp.user.profile.phone_number_1 || '');
 | 
				
			||||||
              $('#id_phone_number_2').val(resp.user.profile.phone_number_2 || '');
 | 
					              $('#id_phone_number_2').val(resp.user.profile.phone_number_2 || '');
 | 
				
			||||||
 | 
					              $('#id_company_name').val(resp.user.profile.company_name || '');
 | 
				
			||||||
 | 
					              $('#id_company_national_id').val(resp.user.profile.company_national_id || '');
 | 
				
			||||||
              $('#id_card_number').val(resp.user.profile.card_number || '');
 | 
					              $('#id_card_number').val(resp.user.profile.card_number || '');
 | 
				
			||||||
              $('#id_account_number').val(resp.user.profile.account_number || '');
 | 
					              $('#id_account_number').val(resp.user.profile.account_number || '');
 | 
				
			||||||
              $('#id_bank_name').val(resp.user.profile.bank_name || '');
 | 
					              $('#id_bank_name').val(resp.user.profile.bank_name || '');
 | 
				
			||||||
              $('#id_address').val(resp.user.profile.address || '');
 | 
					              $('#id_address').val(resp.user.profile.address || '');
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
 | 
					              $('#user-type-select').val('individual');
 | 
				
			||||||
              $('#id_national_code').val(nc);
 | 
					              $('#id_national_code').val(nc);
 | 
				
			||||||
              $('#id_phone_number_1').val('');
 | 
					              $('#id_phone_number_1').val('');
 | 
				
			||||||
              $('#id_phone_number_2').val('');
 | 
					              $('#id_phone_number_2').val('');
 | 
				
			||||||
 | 
					              $('#id_company_name').val('');
 | 
				
			||||||
 | 
					              $('#id_company_national_id').val('');
 | 
				
			||||||
              $('#id_card_number').val('');
 | 
					              $('#id_card_number').val('');
 | 
				
			||||||
              $('#id_account_number').val('');
 | 
					              $('#id_account_number').val('');
 | 
				
			||||||
              $('#id_bank_name').val('');
 | 
					              $('#id_bank_name').val('');
 | 
				
			||||||
              $('#id_address').val('');
 | 
					              $('#id_address').val('');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            toggleRepCompanyFields();
 | 
				
			||||||
            setStatus('#repStatus', 'نماینده یافت شد.', 'success');
 | 
					            setStatus('#repStatus', 'نماینده یافت شد.', 'success');
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            currentRepId = null;
 | 
					            currentRepId = null;
 | 
				
			||||||
            $('#repNewFields').show();
 | 
					            $('#repNewFields').show();
 | 
				
			||||||
            // Clear form and prefill national code
 | 
					            // Clear form and prefill national code
 | 
				
			||||||
 | 
					            $('#user-type-select').val('individual');
 | 
				
			||||||
            $('#id_first_name').val('');
 | 
					            $('#id_first_name').val('');
 | 
				
			||||||
            $('#id_last_name').val('');
 | 
					            $('#id_last_name').val('');
 | 
				
			||||||
            $('#id_national_code').val(nc);
 | 
					            $('#id_national_code').val(nc);
 | 
				
			||||||
            $('#id_phone_number_1').val('');
 | 
					            $('#id_phone_number_1').val('');
 | 
				
			||||||
            $('#id_phone_number_2').val('');
 | 
					            $('#id_phone_number_2').val('');
 | 
				
			||||||
 | 
					            $('#id_company_name').val('');
 | 
				
			||||||
 | 
					            $('#id_company_national_id').val('');
 | 
				
			||||||
            $('#id_card_number').val('');
 | 
					            $('#id_card_number').val('');
 | 
				
			||||||
            $('#id_account_number').val('');
 | 
					            $('#id_account_number').val('');
 | 
				
			||||||
            $('#id_bank_name').val('');
 | 
					            $('#id_bank_name').val('');
 | 
				
			||||||
            $('#id_address').val('');
 | 
					            $('#id_address').val('');
 | 
				
			||||||
 | 
					            toggleRepCompanyFields();
 | 
				
			||||||
            setStatus('#repStatus', 'نماینده یافت نشد. لطفا اطلاعات را تکمیل کنید.', 'danger');
 | 
					            setStatus('#repStatus', 'نماینده یافت نشد. لطفا اطلاعات را تکمیل کنید.', 'danger');
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					@ -954,6 +1008,45 @@
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Export to Excel function
 | 
				
			||||||
 | 
					    window.exportToExcel = function() {
 | 
				
			||||||
 | 
					      // Get current filter parameters from the form
 | 
				
			||||||
 | 
					      const form = document.querySelector('form[method="get"]');
 | 
				
			||||||
 | 
					      const formData = new FormData(form);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Build query string
 | 
				
			||||||
 | 
					      const params = new URLSearchParams();
 | 
				
			||||||
 | 
					      for (const [key, value] of formData.entries()) {
 | 
				
			||||||
 | 
					        if (value.trim()) {
 | 
				
			||||||
 | 
					          params.append(key, value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Create export URL with current filters
 | 
				
			||||||
 | 
					      const exportUrl = '{% url "processes:export_requests_excel" %}' + '?' + params.toString();
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Show loading state
 | 
				
			||||||
 | 
					      const btn = document.querySelector('button[onclick="exportToExcel()"]');
 | 
				
			||||||
 | 
					      const originalText = btn.innerHTML;
 | 
				
			||||||
 | 
					      btn.innerHTML = '<i class="bx bx-loader-circle bx-spin me-1"></i>در حال تولید...';
 | 
				
			||||||
 | 
					      btn.disabled = true;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Create invisible link and trigger download
 | 
				
			||||||
 | 
					      const link = document.createElement('a');
 | 
				
			||||||
 | 
					      link.href = exportUrl;
 | 
				
			||||||
 | 
					      link.style.display = 'none';
 | 
				
			||||||
 | 
					      document.body.appendChild(link);
 | 
				
			||||||
 | 
					      link.click();
 | 
				
			||||||
 | 
					      document.body.removeChild(link);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Reset button after a short delay
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        btn.innerHTML = originalText;
 | 
				
			||||||
 | 
					        btn.disabled = false;
 | 
				
			||||||
 | 
					        showToast('فایل اکسل آماده دانلود است', 'success');
 | 
				
			||||||
 | 
					      }, 1000);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,9 +28,11 @@ def stepper_header(instance, current_step=None):
 | 
				
			||||||
        status = step_id_to_status.get(step.id, 'pending')
 | 
					        status = step_id_to_status.get(step.id, 'pending')
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # بررسی دسترسی به مرحله (UI navigation constraints):
 | 
					        # بررسی دسترسی به مرحله (UI navigation constraints):
 | 
				
			||||||
        # can_access = instance.can_access_step(step)
 | 
					        can_access = instance.can_access_step(step)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # فقط مراحل تکمیلشده یا مرحله جاری قابل کلیک هستند
 | 
					        # فقط مراحل تکمیلشده یا مرحله جاری قابل کلیک هستند
 | 
				
			||||||
        can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
 | 
					        # can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # مرحله انتخابشده (نمایش فعلی)
 | 
					        # مرحله انتخابشده (نمایش فعلی)
 | 
				
			||||||
        is_selected = bool(current_step and step.id == current_step.id)
 | 
					        is_selected = bool(current_step and step.id == current_step.id)
 | 
				
			||||||
        # مرحلهای که باید انجام شود (مرحله جاری در instance)
 | 
					        # مرحلهای که باید انجام شود (مرحله جاری در instance)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ app_name = 'processes'
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    # Requests UI
 | 
					    # Requests UI
 | 
				
			||||||
    path('requests/', views.request_list, name='request_list'),
 | 
					    path('requests/', views.request_list, name='request_list'),
 | 
				
			||||||
 | 
					    path('requests/export/excel/', views.export_requests_excel, name='export_requests_excel'),
 | 
				
			||||||
    path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'),
 | 
					    path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'),
 | 
				
			||||||
    path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'),
 | 
					    path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'),
 | 
				
			||||||
    path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'),
 | 
					    path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ def scope_instances_queryset(user, queryset=None):
 | 
				
			||||||
            return qs.filter(id__in=assign_ids)
 | 
					            return qs.filter(id__in=assign_ids)
 | 
				
			||||||
        if profile.has_role(UserRoles.BROKER):
 | 
					        if profile.has_role(UserRoles.BROKER):
 | 
				
			||||||
            return qs.filter(broker=profile.broker)
 | 
					            return qs.filter(broker=profile.broker)
 | 
				
			||||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
					        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
 | 
				
			||||||
            return qs.filter(broker__affairs__county=profile.county)
 | 
					            return qs.filter(broker__affairs__county=profile.county)
 | 
				
			||||||
        if profile.has_role(UserRoles.ADMIN):
 | 
					        if profile.has_role(UserRoles.ADMIN):
 | 
				
			||||||
            return qs
 | 
					            return qs
 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ def scope_wells_queryset(user, queryset=None):
 | 
				
			||||||
            return qs
 | 
					            return qs
 | 
				
			||||||
        if profile.has_role(UserRoles.BROKER):
 | 
					        if profile.has_role(UserRoles.BROKER):
 | 
				
			||||||
            return qs.filter(broker=profile.broker)
 | 
					            return qs.filter(broker=profile.broker)
 | 
				
			||||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
					        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
 | 
				
			||||||
            return qs.filter(broker__affairs__county=profile.county)
 | 
					            return qs.filter(broker__affairs__county=profile.county)
 | 
				
			||||||
        if profile.has_role(UserRoles.INSTALLER):
 | 
					        if profile.has_role(UserRoles.INSTALLER):
 | 
				
			||||||
            # Wells that have instances assigned to this installer
 | 
					            # Wells that have instances assigned to this installer
 | 
				
			||||||
| 
						 | 
					@ -102,7 +102,7 @@ def scope_customers_queryset(user, queryset=None):
 | 
				
			||||||
            return qs
 | 
					            return qs
 | 
				
			||||||
        if profile.has_role(UserRoles.BROKER):
 | 
					        if profile.has_role(UserRoles.BROKER):
 | 
				
			||||||
            return qs.filter(broker=profile.broker)
 | 
					            return qs.filter(broker=profile.broker)
 | 
				
			||||||
        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
 | 
					        if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER):
 | 
				
			||||||
            return qs.filter(county=profile.county)
 | 
					            return qs.filter(county=profile.county)
 | 
				
			||||||
        if profile.has_role(UserRoles.INSTALLER):
 | 
					        if profile.has_role(UserRoles.INSTALLER):
 | 
				
			||||||
            # Customers that are representatives of instances assigned to this installer
 | 
					            # Customers that are representatives of instances assigned to this installer
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,13 +3,19 @@ from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.decorators import login_required
 | 
					from django.contrib.auth.decorators import login_required
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.http import JsonResponse
 | 
					from django.http import JsonResponse, HttpResponse
 | 
				
			||||||
from django.views.decorators.http import require_POST, require_GET
 | 
					from django.views.decorators.http import require_POST, require_GET
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
 | 
					import openpyxl
 | 
				
			||||||
 | 
					from openpyxl.styles import Font, Alignment, PatternFill
 | 
				
			||||||
 | 
					from openpyxl.utils import get_column_letter
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from _helpers.utils import persian_converter3
 | 
				
			||||||
from .models import Process, ProcessInstance, StepInstance, ProcessStep
 | 
					from .models import Process, ProcessInstance, StepInstance, ProcessStep
 | 
				
			||||||
from .utils import scope_instances_queryset, get_scoped_instance_or_404
 | 
					from .utils import scope_instances_queryset, get_scoped_instance_or_404
 | 
				
			||||||
from installations.models import InstallationAssignment
 | 
					from installations.models import InstallationAssignment, InstallationReport
 | 
				
			||||||
from wells.models import Well
 | 
					from wells.models import Well
 | 
				
			||||||
from accounts.models import Profile, Broker
 | 
					from accounts.models import Profile, Broker
 | 
				
			||||||
from locations.models import Affairs
 | 
					from locations.models import Affairs
 | 
				
			||||||
| 
						 | 
					@ -65,18 +71,65 @@ def request_list(request):
 | 
				
			||||||
    steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order')
 | 
					    steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order')
 | 
				
			||||||
    manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
 | 
					    manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Calculate progress for each instance
 | 
					    # Prepare installation assignments map (scheduled date by instance)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        instance_ids = list(instances.values_list('id', flat=True))
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        instance_ids = []
 | 
				
			||||||
 | 
					    assignments_map = {}
 | 
				
			||||||
 | 
					    reports_map = {}
 | 
				
			||||||
 | 
					    if instance_ids:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            ass_qs = InstallationAssignment.objects.filter(process_instance_id__in=instance_ids).values('process_instance_id', 'scheduled_date')
 | 
				
			||||||
 | 
					            for row in ass_qs:
 | 
				
			||||||
 | 
					                assignments_map[row['process_instance_id']] = row['scheduled_date']
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            assignments_map = {}
 | 
				
			||||||
 | 
					        # latest report per instance (visited_date)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            rep_qs = InstallationReport.objects.filter(assignment__process_instance_id__in=instance_ids).order_by('-created').values('assignment__process_instance_id', 'visited_date')
 | 
				
			||||||
 | 
					            for row in rep_qs:
 | 
				
			||||||
 | 
					                pid = row['assignment__process_instance_id']
 | 
				
			||||||
 | 
					                if pid not in reports_map:
 | 
				
			||||||
 | 
					                    reports_map[pid] = row['visited_date']
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            reports_map = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Calculate progress for each instance and attach install schedule info
 | 
				
			||||||
    instances_with_progress = []
 | 
					    instances_with_progress = []
 | 
				
			||||||
    for instance in instances:
 | 
					    for instance in instances:
 | 
				
			||||||
        total_steps = instance.process.steps.count()
 | 
					        total_steps = instance.process.steps.count()
 | 
				
			||||||
        completed_steps = instance.step_instances.filter(status='completed').count()
 | 
					        completed_steps = instance.step_instances.filter(status='completed').count()
 | 
				
			||||||
        progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
					        progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
				
			||||||
        
 | 
					        sched_date = assignments_map.get(instance.id)
 | 
				
			||||||
 | 
					        overdue_days = 0
 | 
				
			||||||
 | 
					        reference_date = None
 | 
				
			||||||
 | 
					        if sched_date:
 | 
				
			||||||
 | 
					            # Reference date: until installer submits a report, use today; otherwise use visited_date
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                visited_date = reports_map.get(instance.id)
 | 
				
			||||||
 | 
					                if visited_date:
 | 
				
			||||||
 | 
					                    reference_date = visited_date
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        reference_date = timezone.localdate()
 | 
				
			||||||
 | 
					                    except Exception:
 | 
				
			||||||
 | 
					                        from datetime import date as _date
 | 
				
			||||||
 | 
					                        reference_date = _date.today()
 | 
				
			||||||
 | 
					                if reference_date > sched_date:
 | 
				
			||||||
 | 
					                    overdue_days = (reference_date - sched_date).days
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                overdue_days = 0
 | 
				
			||||||
 | 
					                reference_date = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date
 | 
				
			||||||
        instances_with_progress.append({
 | 
					        instances_with_progress.append({
 | 
				
			||||||
            'instance': instance,
 | 
					            'instance': instance,
 | 
				
			||||||
            'progress_percentage': round(progress_percentage),
 | 
					            'progress_percentage': round(progress_percentage),
 | 
				
			||||||
            'completed_steps': completed_steps,
 | 
					            'completed_steps': completed_steps,
 | 
				
			||||||
            'total_steps': total_steps,
 | 
					            'total_steps': total_steps,
 | 
				
			||||||
 | 
					            'installation_scheduled_date': installation_scheduled_date,
 | 
				
			||||||
 | 
					            'installation_overdue_days': overdue_days,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Summary stats for header cards
 | 
					    # Summary stats for header cards
 | 
				
			||||||
| 
						 | 
					@ -160,7 +213,10 @@ def lookup_representative_by_national_code(request):
 | 
				
			||||||
            'last_name': user.last_name,
 | 
					            'last_name': user.last_name,
 | 
				
			||||||
            'full_name': user.get_full_name(),
 | 
					            'full_name': user.get_full_name(),
 | 
				
			||||||
            'profile': {
 | 
					            'profile': {
 | 
				
			||||||
 | 
					                'user_type': profile.user_type,
 | 
				
			||||||
                'national_code': profile.national_code,
 | 
					                'national_code': profile.national_code,
 | 
				
			||||||
 | 
					                'company_name': profile.company_name,
 | 
				
			||||||
 | 
					                'company_national_id': profile.company_national_id,
 | 
				
			||||||
                'phone_number_1': profile.phone_number_1,
 | 
					                'phone_number_1': profile.phone_number_1,
 | 
				
			||||||
                'phone_number_2': profile.phone_number_2,
 | 
					                'phone_number_2': profile.phone_number_2,
 | 
				
			||||||
                'card_number': profile.card_number,
 | 
					                'card_number': profile.card_number,
 | 
				
			||||||
| 
						 | 
					@ -240,6 +296,7 @@ def create_request_with_entities(request):
 | 
				
			||||||
            well = existing
 | 
					            well = existing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    well_data = request.POST.copy()
 | 
					    well_data = request.POST.copy()
 | 
				
			||||||
 | 
					    print(well_data)
 | 
				
			||||||
    # Ensure representative set from created/selected user if not provided
 | 
					    # Ensure representative set from created/selected user if not provided
 | 
				
			||||||
    if representative_user and not well_data.get('representative'):
 | 
					    if representative_user and not well_data.get('representative'):
 | 
				
			||||||
        well_data['representative'] = str(representative_user.id)
 | 
					        well_data['representative'] = str(representative_user.id)
 | 
				
			||||||
| 
						 | 
					@ -366,12 +423,12 @@ def step_detail(request, instance_id, step_id):
 | 
				
			||||||
        return redirect('processes:instance_summary', instance_id=instance.id)
 | 
					        return redirect('processes:instance_summary', instance_id=instance.id)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیلشده
 | 
					    # جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیلشده
 | 
				
			||||||
    try:
 | 
					    # try:
 | 
				
			||||||
        if instance.current_step and step.order > instance.current_step.order:
 | 
					    #     if instance.current_step and step.order > instance.current_step.order:
 | 
				
			||||||
            messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.')
 | 
					    #         messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.')
 | 
				
			||||||
            return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
 | 
					    #         return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
 | 
				
			||||||
    except Exception:
 | 
					    # except Exception:
 | 
				
			||||||
        pass
 | 
					    #     pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # بررسی دسترسی به مرحله
 | 
					    # بررسی دسترسی به مرحله
 | 
				
			||||||
    if not instance.can_access_step(step):
 | 
					    if not instance.can_access_step(step):
 | 
				
			||||||
| 
						 | 
					@ -471,4 +528,365 @@ def instance_summary(request, instance_id):
 | 
				
			||||||
        'latest_report': latest_report,
 | 
					        'latest_report': latest_report,
 | 
				
			||||||
        'certificate': certificate,
 | 
					        'certificate': certificate,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_date_jalali(date_obj):
 | 
				
			||||||
 | 
					    """Convert date to Jalali format without time"""
 | 
				
			||||||
 | 
					    if not date_obj:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # If it's a datetime, get just the date part
 | 
				
			||||||
 | 
					        if hasattr(date_obj, 'date'):
 | 
				
			||||||
 | 
					            date_obj = date_obj.date()
 | 
				
			||||||
 | 
					        return persian_converter3(date_obj)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_datetime_jalali(datetime_obj):
 | 
				
			||||||
 | 
					    """Convert datetime to Jalali format without time"""
 | 
				
			||||||
 | 
					    if not datetime_obj:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Get just the date part
 | 
				
			||||||
 | 
					        date_part = datetime_obj.date() if hasattr(datetime_obj, 'date') else datetime_obj
 | 
				
			||||||
 | 
					        return persian_converter3(date_part)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def export_requests_excel(request):
 | 
				
			||||||
 | 
					    """Export filtered requests to Excel"""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get the same queryset as request_list view (with filters)
 | 
				
			||||||
 | 
					    instances = ProcessInstance.objects.select_related(
 | 
				
			||||||
 | 
					        'process', 'current_step', 'representative', 'well', 'well__county', 'well__affairs'
 | 
				
			||||||
 | 
					    ).prefetch_related('step_instances')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Apply scoping
 | 
				
			||||||
 | 
					    instances = scope_instances_queryset(request.user, instances)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Apply filters (same logic as request_list view)
 | 
				
			||||||
 | 
					    filter_status = request.GET.get('status', '').strip()
 | 
				
			||||||
 | 
					    if filter_status:
 | 
				
			||||||
 | 
					        instances = instances.filter(status=filter_status)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    filter_affairs = request.GET.get('affairs', '').strip()
 | 
				
			||||||
 | 
					    if filter_affairs and filter_affairs.isdigit():
 | 
				
			||||||
 | 
					        instances = instances.filter(well__affairs_id=filter_affairs)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    filter_broker = request.GET.get('broker', '').strip()
 | 
				
			||||||
 | 
					    if filter_broker and filter_broker.isdigit():
 | 
				
			||||||
 | 
					        instances = instances.filter(well__broker_id=filter_broker)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    filter_step = request.GET.get('step', '').strip()
 | 
				
			||||||
 | 
					    if filter_step and filter_step.isdigit():
 | 
				
			||||||
 | 
					        instances = instances.filter(current_step_id=filter_step)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get installation data
 | 
				
			||||||
 | 
					    assignment_ids = list(instances.values_list('id', flat=True))
 | 
				
			||||||
 | 
					    assignments_map = {}
 | 
				
			||||||
 | 
					    reports_map = {}
 | 
				
			||||||
 | 
					    installers_map = {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if assignment_ids:
 | 
				
			||||||
 | 
					        assignments = InstallationAssignment.objects.filter(
 | 
				
			||||||
 | 
					            process_instance_id__in=assignment_ids
 | 
				
			||||||
 | 
					        ).select_related('process_instance', 'installer')
 | 
				
			||||||
 | 
					        assignments_map = {a.process_instance_id: a.scheduled_date for a in assignments}
 | 
				
			||||||
 | 
					        installers_map = {a.process_instance_id: a.installer for a in assignments}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        reports = InstallationReport.objects.filter(
 | 
				
			||||||
 | 
					            assignment__process_instance_id__in=assignment_ids
 | 
				
			||||||
 | 
					        ).select_related('assignment')
 | 
				
			||||||
 | 
					        reports_map = {r.assignment.process_instance_id: r for r in reports}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get quotes and payments data
 | 
				
			||||||
 | 
					    from invoices.models import Quote, Payment, Invoice
 | 
				
			||||||
 | 
					    quotes_map = {}
 | 
				
			||||||
 | 
					    payments_map = {}
 | 
				
			||||||
 | 
					    settlement_dates_map = {}
 | 
				
			||||||
 | 
					    approval_dates_map = {}
 | 
				
			||||||
 | 
					    approval_users_map = {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if assignment_ids:
 | 
				
			||||||
 | 
					        # Get quotes
 | 
				
			||||||
 | 
					        quotes = Quote.objects.filter(
 | 
				
			||||||
 | 
					            process_instance_id__in=assignment_ids
 | 
				
			||||||
 | 
					        ).select_related('process_instance')
 | 
				
			||||||
 | 
					        quotes_map = {q.process_instance_id: q for q in quotes}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get payments with reference numbers
 | 
				
			||||||
 | 
					        payments = Payment.objects.filter(
 | 
				
			||||||
 | 
					            invoice__process_instance_id__in=assignment_ids, 
 | 
				
			||||||
 | 
					            is_deleted=False
 | 
				
			||||||
 | 
					        ).select_related('invoice__process_instance').order_by('created')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for payment in payments:
 | 
				
			||||||
 | 
					            if payment.invoice.process_instance_id not in payments_map:
 | 
				
			||||||
 | 
					                payments_map[payment.invoice.process_instance_id] = []
 | 
				
			||||||
 | 
					            payments_map[payment.invoice.process_instance_id].append(payment)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get final invoices to check settlement dates
 | 
				
			||||||
 | 
					        invoices = Invoice.objects.filter(
 | 
				
			||||||
 | 
					            process_instance_id__in=assignment_ids
 | 
				
			||||||
 | 
					        ).select_related('process_instance')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for invoice in invoices:
 | 
				
			||||||
 | 
					            if invoice.remaining_amount == 0:  # Fully settled
 | 
				
			||||||
 | 
					                # Find the last payment date for this invoice
 | 
				
			||||||
 | 
					                last_payment = Payment.objects.filter(
 | 
				
			||||||
 | 
					                    invoice__process_instance=invoice.process_instance,
 | 
				
			||||||
 | 
					                    is_deleted=False
 | 
				
			||||||
 | 
					                ).order_by('-created').first()
 | 
				
			||||||
 | 
					                if last_payment:
 | 
				
			||||||
 | 
					                    settlement_dates_map[invoice.process_instance_id] = last_payment.created
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get installation approval data
 | 
				
			||||||
 | 
					        from processes.models import StepInstance, StepApproval
 | 
				
			||||||
 | 
					        installation_steps = StepInstance.objects.filter(
 | 
				
			||||||
 | 
					            process_instance_id__in=assignment_ids,
 | 
				
			||||||
 | 
					            step__slug='installation_report',  # Assuming this is the slug for installation step
 | 
				
			||||||
 | 
					            status='completed'
 | 
				
			||||||
 | 
					        ).select_related('process_instance')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for step_instance in installation_steps:
 | 
				
			||||||
 | 
					            # Get the approval that completed this step
 | 
				
			||||||
 | 
					            approval = StepApproval.objects.filter(
 | 
				
			||||||
 | 
					                step_instance=step_instance,
 | 
				
			||||||
 | 
					                decision='approved',
 | 
				
			||||||
 | 
					                is_deleted=False
 | 
				
			||||||
 | 
					            ).select_related('approved_by').order_by('-created').first()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if approval:
 | 
				
			||||||
 | 
					                approval_dates_map[step_instance.process_instance_id] = approval.created
 | 
				
			||||||
 | 
					                approval_users_map[step_instance.process_instance_id] = approval.approved_by
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Calculate progress and installation data
 | 
				
			||||||
 | 
					    instances_with_progress = []
 | 
				
			||||||
 | 
					    for instance in instances:
 | 
				
			||||||
 | 
					        total_steps = instance.process.steps.count()
 | 
				
			||||||
 | 
					        completed_steps = instance.step_instances.filter(status='completed').count()
 | 
				
			||||||
 | 
					        progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        sched_date = assignments_map.get(instance.id)
 | 
				
			||||||
 | 
					        overdue_days = 0
 | 
				
			||||||
 | 
					        reference_date = None
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if sched_date:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                report = reports_map.get(instance.id)
 | 
				
			||||||
 | 
					                if report and report.visited_date:
 | 
				
			||||||
 | 
					                    reference_date = report.visited_date
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        reference_date = timezone.localdate()
 | 
				
			||||||
 | 
					                    except Exception:
 | 
				
			||||||
 | 
					                        from datetime import date as _date
 | 
				
			||||||
 | 
					                        reference_date = _date.today()
 | 
				
			||||||
 | 
					                if reference_date > sched_date:
 | 
				
			||||||
 | 
					                    overdue_days = (reference_date - sched_date).days
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                overdue_days = 0
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        instances_with_progress.append({
 | 
				
			||||||
 | 
					            'instance': instance,
 | 
				
			||||||
 | 
					            'progress_percentage': round(progress_percentage),
 | 
				
			||||||
 | 
					            'completed_steps': completed_steps,
 | 
				
			||||||
 | 
					            'total_steps': total_steps,
 | 
				
			||||||
 | 
					            'installation_scheduled_date': installation_scheduled_date,
 | 
				
			||||||
 | 
					            'installation_overdue_days': overdue_days,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Create Excel workbook
 | 
				
			||||||
 | 
					    wb = openpyxl.Workbook()
 | 
				
			||||||
 | 
					    ws = wb.active
 | 
				
			||||||
 | 
					    ws.title = "لیست درخواستها"
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Set RTL (Right-to-Left) direction
 | 
				
			||||||
 | 
					    ws.sheet_view.rightToLeft = True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Define column headers
 | 
				
			||||||
 | 
					    headers = [
 | 
				
			||||||
 | 
					        'شناسه',
 | 
				
			||||||
 | 
					        'تاریخ ایجاد درخواست',
 | 
				
			||||||
 | 
					        'نام نماینده',
 | 
				
			||||||
 | 
					        'نام خانوادگی نماینده',
 | 
				
			||||||
 | 
					        'کد ملی نماینده',
 | 
				
			||||||
 | 
					        'نام شرکت',
 | 
				
			||||||
 | 
					        'شناسه شرکت',
 | 
				
			||||||
 | 
					        'سریال کنتور',
 | 
				
			||||||
 | 
					        'سریال کنتور جدید',
 | 
				
			||||||
 | 
					        'شماره اشتراک آب',
 | 
				
			||||||
 | 
					        'شماره اشتراک برق',
 | 
				
			||||||
 | 
					        'قدرت چاه',
 | 
				
			||||||
 | 
					        'شماره تماس ۱',
 | 
				
			||||||
 | 
					        'شماره تماس ۲',
 | 
				
			||||||
 | 
					        'آدرس',
 | 
				
			||||||
 | 
					        'مبلغ پیشفاکتور',
 | 
				
			||||||
 | 
					        'تاریخ واریزیها و کدهای رهگیری',
 | 
				
			||||||
 | 
					        'تاریخ مراجعه نصاب',
 | 
				
			||||||
 | 
					        'تاخیر نصاب',
 | 
				
			||||||
 | 
					        'نام نصاب',
 | 
				
			||||||
 | 
					        'تاریخ تایید نصب توسط مدیر',
 | 
				
			||||||
 | 
					        'نام تایید کننده نصب',
 | 
				
			||||||
 | 
					        'تاریخ تسویه'
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Write headers
 | 
				
			||||||
 | 
					    for col, header in enumerate(headers, 1):
 | 
				
			||||||
 | 
					        cell = ws.cell(row=1, column=col, value=header)
 | 
				
			||||||
 | 
					        cell.font = Font(bold=True)
 | 
				
			||||||
 | 
					        cell.alignment = Alignment(horizontal='center')
 | 
				
			||||||
 | 
					        cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Write data rows
 | 
				
			||||||
 | 
					    for row_num, item in enumerate(instances_with_progress, 2):
 | 
				
			||||||
 | 
					        instance = item['instance']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get representative info
 | 
				
			||||||
 | 
					        rep_first_name = ""
 | 
				
			||||||
 | 
					        rep_last_name = ""
 | 
				
			||||||
 | 
					        rep_national_code = ""
 | 
				
			||||||
 | 
					        rep_phone_1 = ""
 | 
				
			||||||
 | 
					        rep_phone_2 = ""
 | 
				
			||||||
 | 
					        rep_address = ""
 | 
				
			||||||
 | 
					        company_name = ""
 | 
				
			||||||
 | 
					        company_national_id = ""
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if instance.representative:
 | 
				
			||||||
 | 
					            rep_first_name = instance.representative.first_name or ""
 | 
				
			||||||
 | 
					            rep_last_name = instance.representative.last_name or ""
 | 
				
			||||||
 | 
					            if hasattr(instance.representative, 'profile') and instance.representative.profile:
 | 
				
			||||||
 | 
					                profile = instance.representative.profile
 | 
				
			||||||
 | 
					                rep_national_code = profile.national_code or ""
 | 
				
			||||||
 | 
					                rep_phone_1 = profile.phone_number_1 or ""
 | 
				
			||||||
 | 
					                rep_phone_2 = profile.phone_number_2 or ""
 | 
				
			||||||
 | 
					                rep_address = profile.address or ""
 | 
				
			||||||
 | 
					                if profile.user_type == 'legal':
 | 
				
			||||||
 | 
					                    company_name = profile.company_name or ""
 | 
				
			||||||
 | 
					                    company_national_id = profile.company_national_id or ""
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get well info
 | 
				
			||||||
 | 
					        water_subscription = ""
 | 
				
			||||||
 | 
					        electricity_subscription = ""
 | 
				
			||||||
 | 
					        well_power = ""
 | 
				
			||||||
 | 
					        old_meter_serial = ""
 | 
				
			||||||
 | 
					        if instance.well:
 | 
				
			||||||
 | 
					            water_subscription = instance.well.water_subscription_number or ""
 | 
				
			||||||
 | 
					            electricity_subscription = instance.well.electricity_subscription_number or ""
 | 
				
			||||||
 | 
					            well_power = str(instance.well.well_power) if instance.well.well_power else ""
 | 
				
			||||||
 | 
					            old_meter_serial = instance.well.water_meter_serial_number or ""
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get new meter serial from installation report
 | 
				
			||||||
 | 
					        new_meter_serial = ""
 | 
				
			||||||
 | 
					        installer_visit_date = ""
 | 
				
			||||||
 | 
					        report = reports_map.get(instance.id)
 | 
				
			||||||
 | 
					        if report:
 | 
				
			||||||
 | 
					            new_meter_serial = report.new_water_meter_serial or ""
 | 
				
			||||||
 | 
					            installer_visit_date = format_date_jalali(report.visited_date)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get quote amount
 | 
				
			||||||
 | 
					        quote_amount = ""
 | 
				
			||||||
 | 
					        quote = quotes_map.get(instance.id)
 | 
				
			||||||
 | 
					        if quote:
 | 
				
			||||||
 | 
					            quote_amount = str(quote.final_amount) if quote.final_amount else ""
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get payments info
 | 
				
			||||||
 | 
					        payments_info = ""
 | 
				
			||||||
 | 
					        payments = payments_map.get(instance.id, [])
 | 
				
			||||||
 | 
					        if payments:
 | 
				
			||||||
 | 
					            payment_strings = []
 | 
				
			||||||
 | 
					            for payment in payments:
 | 
				
			||||||
 | 
					                date_str = format_datetime_jalali(payment.created)
 | 
				
			||||||
 | 
					                reference_number = payment.reference_number or "بدون کد"
 | 
				
			||||||
 | 
					                payment_strings.append(f"{date_str} - {reference_number}")
 | 
				
			||||||
 | 
					            payments_info = " | ".join(payment_strings)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get installer name
 | 
				
			||||||
 | 
					        installer_name = ""
 | 
				
			||||||
 | 
					        installer = installers_map.get(instance.id)
 | 
				
			||||||
 | 
					        if installer:
 | 
				
			||||||
 | 
					            installer_name = installer.get_full_name() or str(installer)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get overdue days
 | 
				
			||||||
 | 
					        overdue_days = ""
 | 
				
			||||||
 | 
					        if item['installation_overdue_days'] and item['installation_overdue_days'] > 0:
 | 
				
			||||||
 | 
					            overdue_days = str(item['installation_overdue_days'])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get approval info
 | 
				
			||||||
 | 
					        approval_date = ""
 | 
				
			||||||
 | 
					        approval_user = ""
 | 
				
			||||||
 | 
					        approval_date_obj = approval_dates_map.get(instance.id)
 | 
				
			||||||
 | 
					        approval_user_obj = approval_users_map.get(instance.id)
 | 
				
			||||||
 | 
					        if approval_date_obj:
 | 
				
			||||||
 | 
					            approval_date = format_datetime_jalali(approval_date_obj)
 | 
				
			||||||
 | 
					        if approval_user_obj:
 | 
				
			||||||
 | 
					            approval_user = approval_user_obj.get_full_name() or str(approval_user_obj)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get settlement date
 | 
				
			||||||
 | 
					        settlement_date = ""
 | 
				
			||||||
 | 
					        settlement_date_obj = settlement_dates_map.get(instance.id)
 | 
				
			||||||
 | 
					        if settlement_date_obj:
 | 
				
			||||||
 | 
					            settlement_date = format_datetime_jalali(settlement_date_obj)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        row_data = [
 | 
				
			||||||
 | 
					            instance.code,                    # شناسه
 | 
				
			||||||
 | 
					            format_datetime_jalali(instance.created),  # تاریخ ایجاد درخواست
 | 
				
			||||||
 | 
					            rep_first_name,                   # نام نماینده
 | 
				
			||||||
 | 
					            rep_last_name,                    # نام خانوادگی نماینده
 | 
				
			||||||
 | 
					            rep_national_code,                # کد ملی نماینده
 | 
				
			||||||
 | 
					            company_name,                     # نام شرکت
 | 
				
			||||||
 | 
					            company_national_id,              # شناسه شرکت
 | 
				
			||||||
 | 
					            old_meter_serial,                 # سریال کنتور
 | 
				
			||||||
 | 
					            new_meter_serial,                 # سریال کنتور جدید
 | 
				
			||||||
 | 
					            water_subscription,               # شماره اشتراک آب
 | 
				
			||||||
 | 
					            electricity_subscription,         # شماره اشتراک برق
 | 
				
			||||||
 | 
					            well_power,                       # قدرت چاه
 | 
				
			||||||
 | 
					            rep_phone_1,                      # شماره تماس ۱
 | 
				
			||||||
 | 
					            rep_phone_2,                      # شماره تماس ۲
 | 
				
			||||||
 | 
					            rep_address,                      # آدرس
 | 
				
			||||||
 | 
					            quote_amount,                     # مبلغ پیشفاکتور
 | 
				
			||||||
 | 
					            payments_info,                    # تاریخ واریزیها و کدهای رهگیری
 | 
				
			||||||
 | 
					            installer_visit_date,             # تاریخ مراجعه نصاب
 | 
				
			||||||
 | 
					            overdue_days,                     # تاخیر نصاب
 | 
				
			||||||
 | 
					            installer_name,                   # نام نصاب
 | 
				
			||||||
 | 
					            approval_date,                    # تاریخ تایید نصب توسط مدیر
 | 
				
			||||||
 | 
					            approval_user,                    # نام تایید کننده نصب
 | 
				
			||||||
 | 
					            settlement_date                   # تاریخ تسویه
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for col, value in enumerate(row_data, 1):
 | 
				
			||||||
 | 
					            cell = ws.cell(row=row_num, column=col, value=value)
 | 
				
			||||||
 | 
					            # Set right alignment for Persian text
 | 
				
			||||||
 | 
					            cell.alignment = Alignment(horizontal='right')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Auto-adjust column widths
 | 
				
			||||||
 | 
					    for col in range(1, len(headers) + 1):
 | 
				
			||||||
 | 
					        column_letter = get_column_letter(col)
 | 
				
			||||||
 | 
					        max_length = 0
 | 
				
			||||||
 | 
					        for row in ws[column_letter]:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                if len(str(row.value)) > max_length:
 | 
				
			||||||
 | 
					                    max_length = len(str(row.value))
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					        adjusted_width = min(max_length + 2, 50)
 | 
				
			||||||
 | 
					        ws.column_dimensions[column_letter].width = adjusted_width
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Prepare response
 | 
				
			||||||
 | 
					    response = HttpResponse(
 | 
				
			||||||
 | 
					        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Generate filename with current date
 | 
				
			||||||
 | 
					    current_date = datetime.now().strftime('%Y%m%d_%H%M')
 | 
				
			||||||
 | 
					    filename = f'requests_export_{current_date}.xlsx'
 | 
				
			||||||
 | 
					    response['Content-Disposition'] = f'attachment; filename="{filename}"'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Save workbook to response
 | 
				
			||||||
 | 
					    wb.save(response)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return response
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -114,7 +114,7 @@
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant %}
 | 
					  {% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant or request.user|is_water_resource_manager %}
 | 
				
			||||||
    <!-- Customers -->
 | 
					    <!-- Customers -->
 | 
				
			||||||
    <li class="menu-header small text-uppercase">
 | 
					    <li class="menu-header small text-uppercase">
 | 
				
			||||||
      <span class="menu-header-text">مشترکها</span>
 | 
					      <span class="menu-header-text">مشترکها</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -104,7 +104,8 @@ class WellForm(forms.ModelForm):
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'reference_letter_number': forms.TextInput(attrs={
 | 
					            'reference_letter_number': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': 'شماره معرفی نامه'
 | 
					                'placeholder': 'شماره معرفی نامه',
 | 
				
			||||||
 | 
					                'required': True
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'reference_letter_date': forms.DateInput(attrs={
 | 
					            'reference_letter_date': forms.DateInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										33
									
								
								wells/migrations/0002_alter_historicalwell_utm_x_and_more.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								wells/migrations/0002_alter_historicalwell_utm_x_and_more.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-21 07:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('wells', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='utm_x',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='X UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='utm_y',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='Y UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='utm_x',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='X UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='utm_y',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='Y UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-24 11:07
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('wells', '0002_alter_historicalwell_utm_x_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='discharge_pipe_diameter',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قطر لوله آبده (اینچ)'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='driving_force',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='نیرو محرکه چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='meter_size',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='سایز کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='meter_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('smart', 'هوشمند (آبی/برق)'), ('volumetric', 'حجمی')], max_length=20, null=True, verbose_name='نوع کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت)قدرت موتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='post_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='pre_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='sim_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره سیمکارت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='discharge_pipe_diameter',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قطر لوله آبده (اینچ)'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='driving_force',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='نیرو محرکه چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='meter_size',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='سایز کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='meter_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('smart', 'هوشمند (آبی/برق)'), ('volumetric', 'حجمی')], max_length=20, null=True, verbose_name='نوع کنتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت)قدرت موتور'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='post_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='pre_calibration_flow_rate',
 | 
				
			||||||
 | 
					            field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='sim_number',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره سیمکارت'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-09-24 11:15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('wells', '0003_historicalwell_discharge_pipe_diameter_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='discharge_pipe_diameter',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='driving_force',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='meter_size',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='meter_type',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='post_calibration_flow_rate',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='pre_calibration_flow_rate',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='sim_number',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='discharge_pipe_diameter',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='driving_force',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='exploitation_license_number',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='meter_size',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='meter_type',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='motor_power',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='post_calibration_flow_rate',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='pre_calibration_flow_rate',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='sim_number',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='usage_type',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ from processes.utils import scope_wells_queryset
 | 
				
			||||||
from processes.models import ProcessInstance
 | 
					from processes.models import ProcessInstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def well_list(request):
 | 
					def well_list(request):
 | 
				
			||||||
    """نمایش لیست چاهها"""
 | 
					    """نمایش لیست چاهها"""
 | 
				
			||||||
    base = Well.objects.select_related(
 | 
					    base = Well.objects.select_related(
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ def well_list(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_POST
 | 
					@require_POST
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def add_well_ajax(request):
 | 
					def add_well_ajax(request):
 | 
				
			||||||
    """AJAX endpoint for adding wells"""
 | 
					    """AJAX endpoint for adding wells"""
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,7 @@ def add_well_ajax(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_POST
 | 
					@require_POST
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def edit_well_ajax(request, well_id):
 | 
					def edit_well_ajax(request, well_id):
 | 
				
			||||||
    """AJAX endpoint for editing wells"""
 | 
					    """AJAX endpoint for editing wells"""
 | 
				
			||||||
    well = get_object_or_404(Well, id=well_id)
 | 
					    well = get_object_or_404(Well, id=well_id)
 | 
				
			||||||
| 
						 | 
					@ -154,7 +154,7 @@ def edit_well_ajax(request, well_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_POST
 | 
					@require_POST
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def delete_well(request, well_id):
 | 
					def delete_well(request, well_id):
 | 
				
			||||||
    """حذف چاه"""
 | 
					    """حذف چاه"""
 | 
				
			||||||
    well = get_object_or_404(Well, id=well_id)
 | 
					    well = get_object_or_404(Well, id=well_id)
 | 
				
			||||||
| 
						 | 
					@ -199,7 +199,7 @@ def get_well_data(request, well_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def get_well_details(request, well_id):
 | 
					def get_well_details(request, well_id):
 | 
				
			||||||
    """جزئیات کامل چاه برای نمایش در مدال"""
 | 
					    """جزئیات کامل چاه برای نمایش در مدال"""
 | 
				
			||||||
    well = get_object_or_404(
 | 
					    well = get_object_or_404(
 | 
				
			||||||
| 
						 | 
					@ -260,7 +260,7 @@ def get_well_details(request, well_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@require_GET
 | 
					@require_GET
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
 | 
					@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER])
 | 
				
			||||||
def get_well_requests(request, well_id):
 | 
					def get_well_requests(request, well_id):
 | 
				
			||||||
    """سوابق درخواستهای مرتبط با یک چاه"""
 | 
					    """سوابق درخواستهای مرتبط با یک چاه"""
 | 
				
			||||||
    # Scoped access: reuse base scoping by filtering on ProcessInstance via broker/affairs of current user if needed
 | 
					    # Scoped access: reuse base scoping by filtering on ProcessInstance via broker/affairs of current user if needed
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue