Merge remote-tracking branch 'origin' into shafafiyat/production
This commit is contained in:
		
						commit
						241f56f550
					
				
					 27 changed files with 498 additions and 161 deletions
				
			
		| 
						 | 
					@ -39,7 +39,8 @@ class CustomerForm(forms.ModelForm):
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'phone_number_1': forms.TextInput(attrs={
 | 
					            'phone_number_1': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': '09123456789'
 | 
					                'placeholder': '09123456789',
 | 
				
			||||||
 | 
					                'required': True
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'phone_number_2': forms.TextInput(attrs={
 | 
					            'phone_number_2': forms.TextInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-10-02 09:32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0007_historicalprofile_company_name_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalprofile',
 | 
				
			||||||
 | 
					            name='phone_number_1',
 | 
				
			||||||
 | 
					            field=models.CharField(default=1, max_length=11, verbose_name='شماره تماس ۱'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='profile',
 | 
				
			||||||
 | 
					            name='phone_number_1',
 | 
				
			||||||
 | 
					            field=models.CharField(default=1, max_length=11, verbose_name='شماره تماس ۱'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -78,9 +78,7 @@ class Profile(BaseModel):
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    phone_number_1 = models.CharField(
 | 
					    phone_number_1 = models.CharField(
 | 
				
			||||||
        max_length=11,
 | 
					        max_length=11,
 | 
				
			||||||
        null=True,
 | 
					 | 
				
			||||||
        verbose_name="شماره تماس ۱",
 | 
					        verbose_name="شماره تماس ۱",
 | 
				
			||||||
        blank=True
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    phone_number_2 = models.CharField(
 | 
					    phone_number_2 = models.CharField(
 | 
				
			||||||
        max_length=11,
 | 
					        max_length=11,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ from django.contrib import messages
 | 
				
			||||||
from django.http import JsonResponse
 | 
					from django.http import JsonResponse
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					from django.template import Template, Context
 | 
				
			||||||
 | 
					from django.utils.safestring import mark_safe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from processes.models import ProcessInstance, StepInstance
 | 
					from processes.models import ProcessInstance, StepInstance
 | 
				
			||||||
from invoices.models import Invoice
 | 
					from invoices.models import Invoice
 | 
				
			||||||
| 
						 | 
					@ -28,20 +30,33 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance):
 | 
				
			||||||
    well = instance.well
 | 
					    well = instance.well
 | 
				
			||||||
    rep = instance.representative
 | 
					    rep = instance.representative
 | 
				
			||||||
    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
					    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
				
			||||||
 | 
					    individual = True if rep.profile and rep.profile.user_type == 'individual' else False
 | 
				
			||||||
 | 
					    customer_company_name = rep.profile.company_name if rep.profile and rep.profile.user_type == 'legal' else None
 | 
				
			||||||
 | 
					    city = template.company.broker.affairs.county.city.name if template.company and template.company.broker and template.company.broker.affairs and template.company.broker.affairs.county and template.company.broker.affairs.county.city else None
 | 
				
			||||||
 | 
					    county = template.company.broker.affairs.county.name if template.company and template.company.broker and template.company.broker.affairs and template.company.broker.affairs.county else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx = {
 | 
					    ctx = {
 | 
				
			||||||
        'today_jalali': _to_jalali(timezone.now().date()),
 | 
					        'today_jalali': mark_safe(f"<span class=\"fw-bold\">{_to_jalali(timezone.now().date())}</span>"),
 | 
				
			||||||
        'request_code': instance.code,
 | 
					        'request_code': mark_safe(f"<span class=\"fw-bold\">{instance.code}</span>"),
 | 
				
			||||||
        'company_name': (template.company.name if template.company else '') or '',
 | 
					        'company_name': mark_safe(f"<span class=\"fw-bold\">{(template.company.name if template.company else '') or ''}</span>"),
 | 
				
			||||||
        'customer_full_name': rep.get_full_name() if rep else '',
 | 
					        'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{rep.get_full_name() if rep else ''}</span>"),
 | 
				
			||||||
        'water_subscription_number': getattr(well, 'water_subscription_number', '') or '',
 | 
					        'water_subscription_number': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'water_subscription_number', '') or ''}</span>"),
 | 
				
			||||||
        'address': getattr(well, 'county', '') or '',
 | 
					        'address': mark_safe(f"<span class=\"fw-bold\">{getattr(well, 'county', '') or ''}</span>"),
 | 
				
			||||||
        'visit_date_jalali': _to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else '',
 | 
					        'visit_date_jalali': mark_safe(f"<span class=\"fw-bold\">{_to_jalali(getattr(latest_report, 'visited_date', None)) if latest_report else ''}</span>"),
 | 
				
			||||||
 | 
					        'city': mark_safe(f"<span class=\"fw-bold\">{city or ''}</span>"),
 | 
				
			||||||
 | 
					        'county': mark_safe(f"<span class=\"fw-bold\">{county or ''}</span>"),
 | 
				
			||||||
 | 
					        'customer_company_name': mark_safe(f"<span class=\"fw-bold\">{customer_company_name or ''}</span>"),
 | 
				
			||||||
 | 
					        'individual': individual,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    title = (template.title or '').format(**ctx)
 | 
					    
 | 
				
			||||||
    body = (template.body or '')
 | 
					    # Render title using Django template engine
 | 
				
			||||||
    # Render body placeholders with bold values
 | 
					    title_template = Template(template.title or '')
 | 
				
			||||||
    for k, v in ctx.items():
 | 
					    title = title_template.render(Context(ctx))
 | 
				
			||||||
        body = body.replace(f"{{{{ {k} }}}}", f"<strong>{str(v)}</strong>")
 | 
					    
 | 
				
			||||||
 | 
					    # Render body using Django template engine
 | 
				
			||||||
 | 
					    body_template = Template(template.body or '')
 | 
				
			||||||
 | 
					    body = body_template.render(Context(ctx))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    return title, body
 | 
					    return title, body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +78,7 @@ def certificate_step(request, instance_id, step_id):
 | 
				
			||||||
    if inv:
 | 
					    if inv:
 | 
				
			||||||
        if prev_si and not prev_si.status == 'approved':
 | 
					        if prev_si and not prev_si.status == 'approved':
 | 
				
			||||||
            inv.calculate_totals()
 | 
					            inv.calculate_totals()
 | 
				
			||||||
            if inv.remaining_amount != 0:
 | 
					            if inv.get_remaining_amount() != 0:
 | 
				
			||||||
                messages.error(request, 'مانده فاکتور باید صفر باشد')
 | 
					                messages.error(request, 'مانده فاکتور باید صفر باشد')
 | 
				
			||||||
                return redirect('processes:request_list')
 | 
					                return redirect('processes:request_list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db.sqlite3
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -75,7 +75,6 @@
 | 
				
			||||||
                <i class="bx bx-error-circle me-2"></i>
 | 
					                <i class="bx bx-error-circle me-2"></i>
 | 
				
			||||||
                <div>
 | 
					                <div>
 | 
				
			||||||
                  <div><strong>این گزارش رد شده است.</strong></div>
 | 
					                  <div><strong>این گزارش رد شده است.</strong></div>
 | 
				
			||||||
                  <div class="mt-1 small">علت رد: {{ step_instance.get_latest_rejection.reason }}</div>
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              {% endif %}
 | 
					              {% endif %}
 | 
				
			||||||
| 
						 | 
					@ -99,9 +98,9 @@
 | 
				
			||||||
                  <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-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-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-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.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>
 | 
					                  <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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -320,15 +320,29 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
        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', 'approved_by').filter(is_deleted=False))
 | 
					    approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
 | 
				
			||||||
 | 
					    rejections_list = list(step_instance.rejections.select_related('role', 'rejected_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 = [
 | 
					    rejections_by_role = {r.role_id: r for r in rejections_list}
 | 
				
			||||||
        {
 | 
					    approver_statuses = []
 | 
				
			||||||
 | 
					    for r in reqs:
 | 
				
			||||||
 | 
					        appr = approvals_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        rejection = rejections_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if appr:
 | 
				
			||||||
 | 
					            status = 'approved'
 | 
				
			||||||
 | 
					            reason = appr.reason
 | 
				
			||||||
 | 
					        elif rejection:
 | 
				
			||||||
 | 
					            status = 'rejected'
 | 
				
			||||||
 | 
					            reason = rejection.reason
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            status = None
 | 
				
			||||||
 | 
					            reason = ''
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        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': status,
 | 
				
			||||||
            'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
 | 
					            'reason': reason,
 | 
				
			||||||
        }
 | 
					        })
 | 
				
			||||||
        for r in reqs
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Determine if current user has already approved/rejected (to disable buttons)
 | 
					    # Determine if current user has already approved/rejected (to disable buttons)
 | 
				
			||||||
    current_user_has_decided = False
 | 
					    current_user_has_decided = False
 | 
				
			||||||
| 
						 | 
					@ -356,10 +370,11 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if action == 'approve':
 | 
					        if action == 'approve':
 | 
				
			||||||
            # Record this user's approval for their role
 | 
					            # Record this user's approval for their role
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepApproval.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
					                approved_by=request.user,
 | 
				
			||||||
 | 
					                reason=''
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            # Only mark report approved when ALL required roles have approved
 | 
					            # Only mark report approved when ALL required roles have approved
 | 
				
			||||||
            if step_instance.is_fully_approved():
 | 
					            if step_instance.is_fully_approved():
 | 
				
			||||||
| 
						 | 
					@ -386,12 +401,8 @@ def installation_report_step(request, instance_id, step_id):
 | 
				
			||||||
            if not reason:
 | 
					            if not reason:
 | 
				
			||||||
                messages.error(request, 'لطفاً علت رد شدن را وارد کنید.')
 | 
					                messages.error(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)
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            # Only create StepRejection for rejections, not StepApproval
 | 
				
			||||||
                step_instance=step_instance,
 | 
					            StepRejection.objects.create(step_instance=step_instance, role=matching_role, rejected_by=request.user, reason=reason)
 | 
				
			||||||
                role=matching_role,
 | 
					 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
					 | 
				
			||||||
            existing_report.approved = False
 | 
					            existing_report.approved = False
 | 
				
			||||||
            existing_report.save()
 | 
					            existing_report.save()
 | 
				
			||||||
            # If current step moved ahead of this step, reset it back for correction (align with invoices)
 | 
					            # If current step moved ahead of this step, reset it back for correction (align with invoices)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,11 +44,11 @@ class PaymentInline(admin.TabularInline):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(Invoice)
 | 
					@admin.register(Invoice)
 | 
				
			||||||
class InvoiceAdmin(SimpleHistoryAdmin):
 | 
					class InvoiceAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
    list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount', 'remaining_amount', 'due_date']
 | 
					    list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount_display', 'remaining_amount_display', 'due_date']
 | 
				
			||||||
    list_filter = ['status', 'created', 'due_date', 'process_instance__process']
 | 
					    list_filter = ['status', 'created', 'due_date', 'process_instance__process']
 | 
				
			||||||
    search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
 | 
					    search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
 | 
				
			||||||
    prepopulated_fields = {'slug': ('name',)}
 | 
					    prepopulated_fields = {'slug': ('name',)}
 | 
				
			||||||
    readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount', 'remaining_amount']
 | 
					    readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount_display', 'remaining_amount_display']
 | 
				
			||||||
    inlines = [InvoiceItemInline, PaymentInline]
 | 
					    inlines = [InvoiceItemInline, PaymentInline]
 | 
				
			||||||
    ordering = ['-created']
 | 
					    ordering = ['-created']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,16 @@ class InvoiceAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
        return mark_safe(obj.get_status_display_with_color())
 | 
					        return mark_safe(obj.get_status_display_with_color())
 | 
				
			||||||
    status_display.short_description = "وضعیت"
 | 
					    status_display.short_description = "وضعیت"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def paid_amount_display(self, obj):
 | 
				
			||||||
 | 
					        return f"{obj.get_paid_amount():,.0f} تومان"
 | 
				
			||||||
 | 
					    paid_amount_display.short_description = "مبلغ پرداخت شده"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def remaining_amount_display(self, obj):
 | 
				
			||||||
 | 
					        amount = obj.get_remaining_amount()
 | 
				
			||||||
 | 
					        color = "green" if amount <= 0 else "red"
 | 
				
			||||||
 | 
					        return format_html('<span style="color: {};">{:,.0f} تومان</span>', color, amount)
 | 
				
			||||||
 | 
					    remaining_amount_display.short_description = "مبلغ باقیمانده"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(Payment)
 | 
					@admin.register(Payment)
 | 
				
			||||||
class PaymentAdmin(SimpleHistoryAdmin):
 | 
					class PaymentAdmin(SimpleHistoryAdmin):
 | 
				
			||||||
    list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
 | 
					    list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-10-04 08:16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('invoices', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalinvoice',
 | 
				
			||||||
 | 
					            name='paid_amount',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='historicalinvoice',
 | 
				
			||||||
 | 
					            name='remaining_amount',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='invoice',
 | 
				
			||||||
 | 
					            name='paid_amount',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='invoice',
 | 
				
			||||||
 | 
					            name='remaining_amount',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -228,18 +228,6 @@ class Invoice(NameSlugModel):
 | 
				
			||||||
        default=0, 
 | 
					        default=0, 
 | 
				
			||||||
        verbose_name="مبلغ نهایی"
 | 
					        verbose_name="مبلغ نهایی"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    paid_amount = models.DecimalField(
 | 
					 | 
				
			||||||
        max_digits=15, 
 | 
					 | 
				
			||||||
        decimal_places=2, 
 | 
					 | 
				
			||||||
        default=0, 
 | 
					 | 
				
			||||||
        verbose_name="مبلغ پرداخت شده"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    remaining_amount = models.DecimalField(
 | 
					 | 
				
			||||||
        max_digits=15, 
 | 
					 | 
				
			||||||
        decimal_places=2, 
 | 
					 | 
				
			||||||
        default=0, 
 | 
					 | 
				
			||||||
        verbose_name="مبلغ باقیمانده"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    due_date = models.DateField(verbose_name="تاریخ سررسید")
 | 
					    due_date = models.DateField(verbose_name="تاریخ سررسید")
 | 
				
			||||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
					    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
				
			||||||
    created_by = models.ForeignKey(
 | 
					    created_by = models.ForeignKey(
 | 
				
			||||||
| 
						 | 
					@ -278,22 +266,31 @@ class Invoice(NameSlugModel):
 | 
				
			||||||
        vat_amount = base_amount * vat_rate
 | 
					        vat_amount = base_amount * vat_rate
 | 
				
			||||||
        self.final_amount = base_amount + vat_amount
 | 
					        self.final_amount = base_amount + vat_amount
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # خالص مانده به نفع شرکت (مثبت) یا به نفع مشتری (منفی)
 | 
					        # وضعیت بر اساس مانده خالص (استفاده از تابعها)
 | 
				
			||||||
        net_due = self.final_amount - self.paid_amount
 | 
					        paid = self.get_paid_amount()
 | 
				
			||||||
        self.remaining_amount = net_due
 | 
					        net_due = self.final_amount - paid
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # وضعیت بر اساس مانده خالص
 | 
					 | 
				
			||||||
        if net_due == 0:
 | 
					        if net_due == 0:
 | 
				
			||||||
            self.status = 'paid'
 | 
					            self.status = 'paid'
 | 
				
			||||||
        elif net_due > 0:
 | 
					        elif net_due > 0:
 | 
				
			||||||
            # مشتری هنوز باید پرداخت کند
 | 
					            # مشتری هنوز باید پرداخت کند
 | 
				
			||||||
            self.status = 'partially_paid' if self.paid_amount > 0 else 'sent'
 | 
					            self.status = 'partially_paid' if paid > 0 else 'sent'
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            # شرکت باید به مشتری پرداخت کند
 | 
					            # شرکت باید به مشتری پرداخت کند
 | 
				
			||||||
            self.status = 'partially_paid'
 | 
					            self.status = 'partially_paid'
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_paid_amount(self):
 | 
				
			||||||
 | 
					        """مبلغ پرداخت شده بر اساس پرداختها (مثل Quote)"""
 | 
				
			||||||
 | 
					        return sum((p.amount if p.direction == 'in' else -p.amount) for p in self.payments.filter(is_deleted=False).all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_remaining_amount(self):
 | 
				
			||||||
 | 
					        """مبلغ باقیمانده بر اساس پرداختها (مثل Quote)"""
 | 
				
			||||||
 | 
					        paid = self.get_paid_amount()
 | 
				
			||||||
 | 
					        remaining = self.final_amount - paid
 | 
				
			||||||
 | 
					        return remaining
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_status_display_with_color(self):
 | 
					    def get_status_display_with_color(self):
 | 
				
			||||||
        """نمایش وضعیت با رنگ"""
 | 
					        """نمایش وضعیت با رنگ"""
 | 
				
			||||||
| 
						 | 
					@ -373,17 +370,13 @@ class Payment(BaseModel):
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """بروزرسانی مبالغ فاکتور"""
 | 
					        """بروزرسانی مبالغ فاکتور"""
 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
        # بروزرسانی مبلغ پرداخت شده فاکتور
 | 
					        # فقط مجدداً calculate_totals را صدا کن (مثل Quote)
 | 
				
			||||||
        total_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in self.invoice.payments.filter(is_deleted=False).all())
 | 
					 | 
				
			||||||
        self.invoice.paid_amount = total_paid
 | 
					 | 
				
			||||||
        self.invoice.calculate_totals()
 | 
					        self.invoice.calculate_totals()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, using=None, keep_parents=False):
 | 
					    def delete(self, using=None, keep_parents=False):
 | 
				
			||||||
        """حذف نرم و بروزرسانی مبالغ فاکتور پس از حذف"""
 | 
					        """حذف نرم و بروزرسانی مبالغ فاکتور پس از حذف"""
 | 
				
			||||||
        result = super().delete(using=using, keep_parents=keep_parents)
 | 
					        result = super().delete(using=using, keep_parents=keep_parents)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            total_paid = sum((p.amount if p.direction == 'in' else -p.amount) for p in self.invoice.payments.filter(is_deleted=False).all())
 | 
					 | 
				
			||||||
            self.invoice.paid_amount = total_paid
 | 
					 | 
				
			||||||
            self.invoice.calculate_totals()
 | 
					            self.invoice.calculate_totals()
 | 
				
			||||||
        except Exception:
 | 
					        except Exception:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -159,11 +159,11 @@
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          <tr class="total-section">
 | 
					          <tr class="total-section">
 | 
				
			||||||
            <td colspan="5" class="text-end"><strong>پرداختیها(تومان):</strong></td>
 | 
					            <td colspan="5" class="text-end"><strong>پرداختیها(تومان):</strong></td>
 | 
				
			||||||
            <td><strong">{{ invoice.paid_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
					            <td><strong">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          <tr class="total-section">
 | 
					          <tr class="total-section">
 | 
				
			||||||
            <td colspan="5" class="text-end"><strong>مانده(تومان):</strong></td>
 | 
					            <td colspan="5" class="text-end"><strong>مانده(تومان):</strong></td>
 | 
				
			||||||
            <td><strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
					            <td><strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }}</strong></td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </tfoot>
 | 
					        </tfoot>
 | 
				
			||||||
      </table>
 | 
					      </table>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -74,17 +74,17 @@
 | 
				
			||||||
            <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 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                <div class="h5 mt-1 text-success">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <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 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                <div class="h5 mt-1 {% if invoice.get_remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-6 col-md-3 d-flex align-items-center">
 | 
					            <div class="col-6 col-md-3 d-flex align-items-center">
 | 
				
			||||||
              {% if invoice.remaining_amount <= 0 %}
 | 
					              {% if invoice.get_remaining_amount <= 0 %}
 | 
				
			||||||
                <span class="badge bg-success">تسویه کامل</span>
 | 
					                <span class="badge bg-success">تسویه کامل</span>
 | 
				
			||||||
              {% else %}
 | 
					              {% else %}
 | 
				
			||||||
                <span class="badge bg-warning text-dark">باقیمانده دارد</span>
 | 
					                <span class="badge bg-warning text-dark">باقیمانده دارد</span>
 | 
				
			||||||
| 
						 | 
					@ -165,11 +165,11 @@
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
                <tr>
 | 
					                <tr>
 | 
				
			||||||
                  <th colspan="6" class="text-end">پرداختیها</th>
 | 
					                  <th colspan="6" class="text-end">پرداختیها</th>
 | 
				
			||||||
                  <th class="text-end">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
					                  <th class="text-end">{{ invoice.get_paid_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 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
					                  <th class="text-end {% if invoice.get_remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان</th>
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
              </tfoot>
 | 
					              </tfoot>
 | 
				
			||||||
            </table>
 | 
					            </table>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,7 @@
 | 
				
			||||||
          <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 %}
 | 
					          {% if request.user|is_manager and step_instance.status != 'approved' and step_instance.status != 'completed' and invoice.get_remaining_amount != 0 %}
 | 
				
			||||||
          <button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#forceApproveModal">
 | 
					          <button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#forceApproveModal">
 | 
				
			||||||
            <i class="bx bx-bolt-circle me-1"></i> تایید اضطراری
 | 
					            <i class="bx bx-bolt-circle me-1"></i> تایید اضطراری
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
| 
						 | 
					@ -128,17 +128,17 @@
 | 
				
			||||||
                <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 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                    <div class="h5 mt-1 text-success">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <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 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
					                    <div class="h5 mt-1 {% if invoice.get_remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="col-6 d-flex align-items-center">
 | 
					                <div class="col-6 d-flex align-items-center">
 | 
				
			||||||
                  {% if invoice.remaining_amount <= 0 %}
 | 
					                  {% if invoice.get_remaining_amount <= 0 %}
 | 
				
			||||||
                    <span class="badge bg-success">تسویه کامل</span>
 | 
					                    <span class="badge bg-success">تسویه کامل</span>
 | 
				
			||||||
                  {% else %}
 | 
					                  {% else %}
 | 
				
			||||||
                    <span class="badge bg-warning text-dark">باقیمانده دارد</span>
 | 
					                    <span class="badge bg-warning text-dark">باقیمانده دارد</span>
 | 
				
			||||||
| 
						 | 
					@ -318,9 +318,9 @@
 | 
				
			||||||
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
					          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="modal-body">
 | 
					        <div class="modal-body">
 | 
				
			||||||
          {% if invoice.remaining_amount != 0 %}
 | 
					          {% if invoice.get_remaining_amount != 0 %}
 | 
				
			||||||
            <div class="alert alert-warning" role="alert">
 | 
					            <div class="alert alert-warning" role="alert">
 | 
				
			||||||
              مانده فاکتور: <strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
 | 
					              مانده فاکتور: <strong>{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
 | 
				
			||||||
              امکان تایید تا تسویه کامل فاکتور وجود ندارد.
 | 
					              امکان تایید تا تسویه کامل فاکتور وجود ندارد.
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          {% else %}
 | 
					          {% else %}
 | 
				
			||||||
| 
						 | 
					@ -329,7 +329,7 @@
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="modal-footer">
 | 
					        <div class="modal-footer">
 | 
				
			||||||
          <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
					          <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
 | 
				
			||||||
          <button type="submit" class="btn btn-success" {% if invoice.remaining_amount != 0 %}disabled{% endif %}>تایید</button>
 | 
					          <button type="submit" class="btn btn-success" {% if invoice.get_remaining_amount != 0 %}disabled{% endif %}>تایید</button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,7 +145,17 @@
 | 
				
			||||||
        <!-- Customer & Well Info (compact to match preview) -->
 | 
					        <!-- Customer & Well Info (compact to match preview) -->
 | 
				
			||||||
        <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.user_type == 'legal' %}
 | 
				
			||||||
 | 
					                      اطلاعات مشترک (حقوقی)
 | 
				
			||||||
 | 
					                    {% else %}
 | 
				
			||||||
 | 
					                      اطلاعات مشترک (حقیقی)
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                </h6>
 | 
				
			||||||
 | 
					                {% if 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> {{ quote.customer.get_full_name }}</div>
 | 
					                <div class="small mb-1"><span class="text-muted">نام:</span> {{ quote.customer.get_full_name }}</div>
 | 
				
			||||||
                {% if instance.representative.profile and instance.representative.profile.national_code %}
 | 
					                {% 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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@
 | 
				
			||||||
                  <div class="alert alert-info">
 | 
					                  <div class="alert alert-info">
 | 
				
			||||||
                    <h6>پیشفاکتور موجود</h6>
 | 
					                    <h6>پیشفاکتور موجود</h6>
 | 
				
			||||||
                    <span class="mb-1">{{ existing_quote.name }} | </span>
 | 
					                    <span class="mb-1">{{ existing_quote.name }} | </span>
 | 
				
			||||||
                    <span class="mb-1">مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
 | 
					                    <span class="mb-1">مبلغ کل (با احتساب مالیات): {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
 | 
				
			||||||
                    <span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
 | 
					                    <span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -358,14 +358,28 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
    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', 'approved_by').filter(is_deleted=False))
 | 
					    approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
 | 
				
			||||||
 | 
					    rejections_list = list(step_instance.rejections.select_related('role', 'rejected_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}
 | 
				
			||||||
 | 
					    rejections_by_role = {r.role_id: r for r in rejections_list}
 | 
				
			||||||
    approver_statuses = []
 | 
					    approver_statuses = []
 | 
				
			||||||
    for r in reqs:
 | 
					    for r in reqs:
 | 
				
			||||||
        appr = approvals_by_role.get(r.role_id)
 | 
					        appr = approvals_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        rejection = rejections_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if appr:
 | 
				
			||||||
 | 
					            status = 'approved'
 | 
				
			||||||
 | 
					            reason = appr.reason
 | 
				
			||||||
 | 
					        elif rejection:
 | 
				
			||||||
 | 
					            status = 'rejected'
 | 
				
			||||||
 | 
					            reason = rejection.reason
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            status = None
 | 
				
			||||||
 | 
					            reason = ''
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
        approver_statuses.append({
 | 
					        approver_statuses.append({
 | 
				
			||||||
            'role': r.role,
 | 
					            'role': r.role,
 | 
				
			||||||
            'status': (appr.decision if appr else None),
 | 
					            'status': status,
 | 
				
			||||||
            'reason': (appr.reason if appr else ''),
 | 
					            'reason': reason,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
					    # dynamic permission: who can approve/reject this step (based on requirements)
 | 
				
			||||||
| 
						 | 
					@ -398,10 +412,11 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        action = request.POST.get('action')
 | 
					        action = request.POST.get('action')
 | 
				
			||||||
        if action == 'approve':
 | 
					        if action == 'approve':
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepApproval.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
					                approved_by=request.user,
 | 
				
			||||||
 | 
					                reason=''
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if step_instance.is_fully_approved():
 | 
					            if step_instance.is_fully_approved():
 | 
				
			||||||
                step_instance.status = 'completed'
 | 
					                step_instance.status = 'completed'
 | 
				
			||||||
| 
						 | 
					@ -422,12 +437,12 @@ def quote_payment_step(request, instance_id, step_id):
 | 
				
			||||||
            if not reason:
 | 
					            if not reason:
 | 
				
			||||||
                messages.error(request, 'علت رد شدن را وارد کنید')
 | 
					                messages.error(request, 'علت رد شدن را وارد کنید')
 | 
				
			||||||
                return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
					                return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepRejection.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
					                rejected_by=request.user,
 | 
				
			||||||
            )
 | 
					                reason=reason
 | 
				
			||||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
					                )
 | 
				
			||||||
            # If current step is ahead of this step, reset it back to this step
 | 
					            # If current step is ahead of this step, reset it back to this step
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
					                if instance.current_step and instance.current_step.order > step.order:
 | 
				
			||||||
| 
						 | 
					@ -927,16 +942,30 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Build approver statuses for template (include reason to display in UI)
 | 
					    # Build approver statuses for template (include reason to display in UI)
 | 
				
			||||||
    reqs = list(step.approver_requirements.select_related('role').all())
 | 
					    reqs = list(step.approver_requirements.select_related('role').all())
 | 
				
			||||||
    approvals = list(step_instance.approvals.select_related('role').all())
 | 
					    approvals = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
 | 
				
			||||||
 | 
					    rejections = list(step_instance.rejections.select_related('role', 'rejected_by').filter(is_deleted=False))
 | 
				
			||||||
    approvals_by_role = {a.role_id: a for a in approvals}
 | 
					    approvals_by_role = {a.role_id: a for a in approvals}
 | 
				
			||||||
    approver_statuses = [
 | 
					    rejections_by_role = {r.role_id: r for r in rejections}
 | 
				
			||||||
        {
 | 
					    approver_statuses = []
 | 
				
			||||||
 | 
					    for r in reqs:
 | 
				
			||||||
 | 
					        appr = approvals_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        rejection = rejections_by_role.get(r.role_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if appr:
 | 
				
			||||||
 | 
					            status = 'approved'
 | 
				
			||||||
 | 
					            reason = appr.reason
 | 
				
			||||||
 | 
					        elif rejection:
 | 
				
			||||||
 | 
					            status = 'rejected'
 | 
				
			||||||
 | 
					            reason = rejection.reason
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            status = None
 | 
				
			||||||
 | 
					            reason = ''
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        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': status,
 | 
				
			||||||
            'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
 | 
					            'reason': reason,
 | 
				
			||||||
        }
 | 
					        })
 | 
				
			||||||
        for r in reqs
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    # dynamic permission to control approve/reject UI
 | 
					    # dynamic permission to control approve/reject UI
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
					        user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
 | 
				
			||||||
| 
						 | 
					@ -949,8 +978,8 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
    # Compute whether current user has already decided (approved/rejected)
 | 
					    # Compute whether current user has already decided (approved/rejected)
 | 
				
			||||||
    current_user_has_decided = False
 | 
					    current_user_has_decided = False
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        user_has_approval = step_instance.approvals.filter(approved_by=request.user).exists()
 | 
					        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).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)
 | 
					        current_user_has_decided = bool(user_has_approval or user_has_rejection)
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        current_user_has_decided = False
 | 
					        current_user_has_decided = False
 | 
				
			||||||
| 
						 | 
					@ -968,13 +997,14 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
        if action == 'approve':
 | 
					        if action == 'approve':
 | 
				
			||||||
            # enforce zero remaining
 | 
					            # enforce zero remaining
 | 
				
			||||||
            invoice.calculate_totals()
 | 
					            invoice.calculate_totals()
 | 
				
			||||||
            if invoice.remaining_amount != 0:
 | 
					            if invoice.get_remaining_amount() != 0:
 | 
				
			||||||
                messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})")
 | 
					                messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.get_remaining_amount()})")
 | 
				
			||||||
                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)
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepApproval.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
 | 
					                approved_by=request.user,
 | 
				
			||||||
 | 
					                reason=''
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if step_instance.is_fully_approved():
 | 
					            if step_instance.is_fully_approved():
 | 
				
			||||||
                step_instance.status = 'completed'
 | 
					                step_instance.status = 'completed'
 | 
				
			||||||
| 
						 | 
					@ -993,12 +1023,12 @@ def final_settlement_step(request, instance_id, step_id):
 | 
				
			||||||
            if not reason:
 | 
					            if not reason:
 | 
				
			||||||
                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)
 | 
				
			||||||
            StepApproval.objects.update_or_create(
 | 
					            StepRejection.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                role=matching_role,
 | 
					                role=matching_role,
 | 
				
			||||||
                defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
 | 
					                rejected_by=request.user,
 | 
				
			||||||
            )
 | 
					                reason=reason
 | 
				
			||||||
            StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
 | 
					                )
 | 
				
			||||||
            # If current step is ahead of this step, reset it back to this step (align behavior with other steps)
 | 
					            # If current step is ahead of this step, reset it back to this step (align behavior with other steps)
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                if instance.current_step and instance.current_step.order > step.order:
 | 
					                if instance.current_step and instance.current_step.order > step.order:
 | 
				
			||||||
| 
						 | 
					@ -1173,8 +1203,8 @@ def add_final_payment(request, instance_id, step_id):
 | 
				
			||||||
        'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
 | 
					        'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
 | 
				
			||||||
        'totals': {
 | 
					        'totals': {
 | 
				
			||||||
            'final_amount': str(invoice.final_amount),
 | 
					            'final_amount': str(invoice.final_amount),
 | 
				
			||||||
            'paid_amount': str(invoice.paid_amount),
 | 
					            'paid_amount': str(invoice.get_paid_amount()),
 | 
				
			||||||
            'remaining_amount': str(invoice.remaining_amount),
 | 
					            'remaining_amount': str(invoice.get_remaining_amount()),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1186,14 +1216,17 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
					    step = get_object_or_404(instance.process.steps, id=step_id)
 | 
				
			||||||
    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
					    invoice = get_object_or_404(Invoice, process_instance=instance)
 | 
				
			||||||
    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
					    payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    # Only BROKER can delete final settlement payments
 | 
					    # Only BROKER can delete final settlement payments
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
 | 
					        if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
 | 
				
			||||||
            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)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Delete payment and recalculate invoice totals
 | 
				
			||||||
    payment.hard_delete()
 | 
					    payment.hard_delete()
 | 
				
			||||||
    invoice.refresh_from_db()
 | 
					    invoice.calculate_totals()  # This is what was missing!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # On delete, return to awaiting approval
 | 
					    # On delete, return to awaiting approval
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -1201,16 +1234,11 @@ 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()
 | 
				
			||||||
        try:
 | 
					        # Clear approvals and rejections (like in quote_payment)
 | 
				
			||||||
            for appr in list(si.approvals.all()):
 | 
					        for appr in list(si.approvals.all()):
 | 
				
			||||||
                appr.delete()
 | 
					            appr.delete()
 | 
				
			||||||
        except Exception:
 | 
					        for rej in list(si.rejections.all()):
 | 
				
			||||||
            pass
 | 
					            rej.delete()
 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            for rej in list(si.rejections.all()):
 | 
					 | 
				
			||||||
                rej.delete()
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1244,7 +1272,7 @@ def delete_final_payment(request, instance_id, step_id, payment_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
 | 
					    return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
 | 
				
			||||||
        'final_amount': str(invoice.final_amount),
 | 
					        'final_amount': str(invoice.final_amount),
 | 
				
			||||||
        'paid_amount': str(invoice.paid_amount),
 | 
					        'paid_amount': str(invoice.get_paid_amount()),
 | 
				
			||||||
        'remaining_amount': str(invoice.remaining_amount),
 | 
					        'remaining_amount': str(invoice.get_remaining_amount()),
 | 
				
			||||||
    }})
 | 
					    }})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -162,9 +162,9 @@ 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', 'is_deleted']
 | 
					    list_display = ['step_instance', 'role', 'rejected_by', 'reason_short', 'created_at', 'is_deleted']
 | 
				
			||||||
    list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
 | 
					    list_filter = ['role', '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', 'role__name']
 | 
				
			||||||
    readonly_fields = ['created_at']
 | 
					    readonly_fields = ['created_at']
 | 
				
			||||||
    ordering = ['-created_at']
 | 
					    ordering = ['-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", "is_deleted")
 | 
					    list_display = ("step_instance", "role", "approved_by", "created_at", "is_deleted")
 | 
				
			||||||
    list_filter = ("decision", "role", "step_instance__step__process")
 | 
					    list_filter = ("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,34 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-10-02 09:32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0008_alter_historicalprofile_phone_number_1_and_more'),
 | 
				
			||||||
 | 
					        ('processes', '0005_alter_historicalstepinstance_status_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterUniqueTogether(
 | 
				
			||||||
 | 
					            name='stepapproval',
 | 
				
			||||||
 | 
					            unique_together=set(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='historicalsteprejection',
 | 
				
			||||||
 | 
					            name='role',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='accounts.role', verbose_name='نقش'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='steprejection',
 | 
				
			||||||
 | 
					            name='role',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.role', verbose_name='نقش'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stepapproval',
 | 
				
			||||||
 | 
					            name='role',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.role', verbose_name='نقش'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-10-02 09:50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('processes', '0006_alter_stepapproval_unique_together_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='stepapproval',
 | 
				
			||||||
 | 
					            name='decision',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='stepapproval',
 | 
				
			||||||
 | 
					            name='reason',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, verbose_name='توضیحات'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -378,7 +378,7 @@ class StepInstance(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_latest_rejection(self):
 | 
					    def get_latest_rejection(self):
 | 
				
			||||||
        """دریافت آخرین رد شدن"""
 | 
					        """دریافت آخرین رد شدن"""
 | 
				
			||||||
        return self.rejections.order_by('-created_at').first()
 | 
					        return self.rejections.filter(is_deleted=False).order_by('-created_at').first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # -------- Multi-role approval helpers --------
 | 
					    # -------- Multi-role approval helpers --------
 | 
				
			||||||
    def required_roles(self):
 | 
					    def required_roles(self):
 | 
				
			||||||
| 
						 | 
					@ -386,8 +386,8 @@ class StepInstance(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def approvals_by_role(self):
 | 
					    def approvals_by_role(self):
 | 
				
			||||||
        decisions = {}
 | 
					        decisions = {}
 | 
				
			||||||
        for a in self.approvals.select_related('role').order_by('created_at'):
 | 
					        for a in self.approvals.filter(is_deleted=False).select_related('role').order_by('created_at'):
 | 
				
			||||||
            decisions[a.role_id] = a.decision
 | 
					            decisions[a.role_id] = 'approved'
 | 
				
			||||||
        return decisions
 | 
					        return decisions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_fully_approved(self) -> bool:
 | 
					    def is_fully_approved(self) -> bool:
 | 
				
			||||||
| 
						 | 
					@ -409,6 +409,7 @@ class StepRejection(models.Model):
 | 
				
			||||||
        related_name='rejections',
 | 
					        related_name='rejections',
 | 
				
			||||||
        verbose_name="نمونه مرحله"
 | 
					        verbose_name="نمونه مرحله"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    role = models.ForeignKey(Role, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="نقش")
 | 
				
			||||||
    rejected_by = models.ForeignKey(
 | 
					    rejected_by = models.ForeignKey(
 | 
				
			||||||
        User, 
 | 
					        User, 
 | 
				
			||||||
        on_delete=models.CASCADE, 
 | 
					        on_delete=models.CASCADE, 
 | 
				
			||||||
| 
						 | 
					@ -431,12 +432,13 @@ class StepRejection(models.Model):
 | 
				
			||||||
        ordering = ['-created_at']
 | 
					        ordering = ['-created_at']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()}"
 | 
					        return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()} ({self.role.name})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """ذخیره با تغییر وضعیت مرحله"""
 | 
					        """ذخیره با تغییر وضعیت مرحله"""
 | 
				
			||||||
        self.step_instance.status = 'rejected'
 | 
					        if self.is_deleted == False:
 | 
				
			||||||
        self.step_instance.save()
 | 
					            self.step_instance.status = 'rejected'
 | 
				
			||||||
 | 
					            self.step_instance.save()
 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def hard_delete(self):
 | 
					    def hard_delete(self):
 | 
				
			||||||
| 
						 | 
					@ -447,7 +449,6 @@ class StepRejection(models.Model):
 | 
				
			||||||
        self.save()
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class StepApproverRequirement(models.Model):
 | 
					class StepApproverRequirement(models.Model):
 | 
				
			||||||
    """Required approver roles for a step."""
 | 
					    """Required approver roles for a step."""
 | 
				
			||||||
    step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
 | 
					    step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
 | 
				
			||||||
| 
						 | 
					@ -466,15 +467,13 @@ class StepApproverRequirement(models.Model):
 | 
				
			||||||
class StepApproval(models.Model):
 | 
					class StepApproval(models.Model):
 | 
				
			||||||
    """Approvals per role for a concrete step instance."""
 | 
					    """Approvals per role for a concrete step instance."""
 | 
				
			||||||
    step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله")
 | 
					    step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله")
 | 
				
			||||||
    role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش")
 | 
					    role = models.ForeignKey(Role, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="نقش")
 | 
				
			||||||
    approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="تاییدکننده")
 | 
					    approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, 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='حذف شده')
 | 
					    is_deleted = models.BooleanField(default=False, verbose_name='حذف شده')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        unique_together = ('step_instance', 'role')
 | 
					 | 
				
			||||||
        verbose_name = 'تایید مرحله'
 | 
					        verbose_name = 'تایید مرحله'
 | 
				
			||||||
        verbose_name_plural = 'تاییدهای مرحله'
 | 
					        verbose_name_plural = 'تاییدهای مرحله'
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -487,4 +486,4 @@ class StepApproval(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"{self.step_instance} - {self.role} - {self.decision}"
 | 
					        return f"{self.step_instance} - {self.role} - تایید شده"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,9 +37,9 @@
 | 
				
			||||||
              <i class="bx bx-printer me-2"></i> پرینت فاکتور
 | 
					              <i class="bx bx-printer me-2"></i> پرینت فاکتور
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          {% endif %}
 | 
					          {% endif %}
 | 
				
			||||||
          <a href="{% url 'certificates:certificate_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
 | 
					          <button type="button" class="btn btn-outline-secondary" 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>
 | 
				
			||||||
            بازگشت
 | 
					            بازگشت
 | 
				
			||||||
| 
						 | 
					@ -57,8 +57,8 @@
 | 
				
			||||||
              {% if invoice %}
 | 
					              {% if invoice %}
 | 
				
			||||||
              <div class="row g-3 mb-3">
 | 
					              <div class="row g-3 mb-3">
 | 
				
			||||||
                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مبلغ نهایی</div><div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
					                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مبلغ نهایی</div><div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
				
			||||||
                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">پرداختیها</div><div class="h5 mt-1 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
					                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">پرداختیها</div><div class="h5 mt-1 text-success">{{ invoice.get_paid_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
				
			||||||
                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مانده</div><div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
					                <div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مانده</div><div class="h5 mt-1 {% if invoice.get_remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.get_remaining_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="table-responsive">
 | 
					              <div class="table-responsive">
 | 
				
			||||||
                <table class="table table-striped mb-0">
 | 
					                <table class="table table-striped mb-0">
 | 
				
			||||||
| 
						 | 
					@ -95,32 +95,113 @@
 | 
				
			||||||
          <div class="card border">
 | 
					          <div class="card border">
 | 
				
			||||||
            <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 latest_report and latest_report.assignment and latest_report.assignment.installer %}
 | 
					              <div class="d-flex align-items-center gap-3">
 | 
				
			||||||
                <span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
 | 
					                {% if installation_delay_days > 0 %}
 | 
				
			||||||
              {% endif %}
 | 
					                  <span class="badge bg-warning text-dark">
 | 
				
			||||||
 | 
					                    <i class="bx bx-time-five bx-xs"></i> {{ installation_delay_days }} روز تاخیر
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                {% elif installation_assignment and latest_report %}
 | 
				
			||||||
 | 
					                  <span class="badge bg-success">
 | 
				
			||||||
 | 
					                    <i class="bx bx-check bx-xs"></i> به موقع
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                {% if latest_report and latest_report.assignment and latest_report.assignment.installer %}
 | 
				
			||||||
 | 
					                  <span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="card-body">
 | 
					            <div class="card-body">
 | 
				
			||||||
              {% if latest_report %}
 | 
					              {% if latest_report %}
 | 
				
			||||||
                <div class="row g-3">
 | 
					                <!-- اطلاعات گزارش نصب -->
 | 
				
			||||||
                  <div class="col-12 col-md-6">
 | 
					                <div class="row g-3 mb-3">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}</p>
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-calendar bx-sm me-2"></i>تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% if installation_assignment.scheduled_date %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-calendar-star bx-sm me-2"></i>تاریخ برنامهریزی: {{ installation_assignment.scheduled_date|to_jalali }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}</p>
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ latest_report.seal_number|default:'-' }}</p>
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ latest_report.seal_number|default:'-' }}</p>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="col-12 col-md-6">
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% if latest_report.sim_number %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-mobile bx-sm me-2"></i>شماره سیمکارت: {{ latest_report.sim_number }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.meter_type %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع کنتور: {{ latest_report.get_meter_type_display }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.meter_size %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-ruler bx-sm me-2"></i>سایز کنتور: {{ latest_report.meter_size }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.water_meter_manufacturer %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-buildings bx-sm me-2"></i>سازنده: {{ latest_report.water_meter_manufacturer.name }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.discharge_pipe_diameter %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-shape-circle bx-sm me-2"></i>قطر لوله آبده: {{ latest_report.discharge_pipe_diameter }} اینچ</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.usage_type %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-droplet bx-sm me-2"></i>نوع مصرف: {{ latest_report.get_usage_type_display }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.driving_force %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-car bx-sm me-2"></i>نیرو محرکه: {{ latest_report.driving_force }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.motor_power %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-tag bx-sm me-2"></i>قدرت موتور: {{ latest_report.motor_power }} کیلووات ساعت</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.exploitation_license_number %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه: {{ latest_report.exploitation_license_number }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.pre_calibration_flow_rate %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate }} لیتر/ثانیه</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  {% if latest_report.post_calibration_flow_rate %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-tachometer bx-sm me-2"></i>دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate }} لیتر/ثانیه</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {% endif %}
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ latest_report.utm_x|default:'-' }}</p>
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ latest_report.utm_x|default:'-' }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <div class="col-12 col-md-4">
 | 
				
			||||||
                    <p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ latest_report.utm_y|default:'-' }}</p>
 | 
					                    <p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ latest_report.utm_y|default:'-' }}</p>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                {% if latest_report.description %}
 | 
					                {% if latest_report.description %}
 | 
				
			||||||
                  <div class="mt-2">
 | 
					                  <div class="mb-3">
 | 
				
			||||||
                    <p class="mb-0"><i class="bx bx-text bx-sm me-2"></i><strong>توضیحات:</strong></p>
 | 
					                    <h6 class="text-primary mb-2"><i class="bx bx-text me-1"></i>توضیحات</h6>
 | 
				
			||||||
                    <div class="text-muted">{{ latest_report.description }}</div>
 | 
					                    <div class="text-muted">{{ latest_report.description }}</div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
                <hr>
 | 
					                
 | 
				
			||||||
                <h6>عکسها</h6>
 | 
					                <h6 class="text-primary mb-2"><i class="bx bx-image me-1"></i>عکسها</h6>
 | 
				
			||||||
                <div class="row">
 | 
					                <div class="row">
 | 
				
			||||||
                  {% for p in latest_report.photos.all %}
 | 
					                  {% for p in latest_report.photos.all %}
 | 
				
			||||||
                    <div class="col-6 col-md-3 mb-2"><img class="img-fluid rounded border" src="{{ p.image.url }}" alt="photo"></div>
 | 
					                    <div class="col-6 col-md-3 mb-2"><img class="img-fluid rounded border" src="{{ p.image.url }}" alt="photo"></div>
 | 
				
			||||||
| 
						 | 
					@ -175,6 +256,30 @@
 | 
				
			||||||
    </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="{{ certificate.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 %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,12 +37,14 @@
 | 
				
			||||||
    <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">
 | 
				
			||||||
 | 
					          {% if not request.user|is_installer %}
 | 
				
			||||||
          <button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()">
 | 
					          <button class="btn btn-label-success me-2" type="button" onclick="exportToExcel()">
 | 
				
			||||||
            <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="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>
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
          {% if request.user|is_broker %}
 | 
					          {% if request.user|is_broker %}
 | 
				
			||||||
          <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
 | 
					          <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
 | 
				
			||||||
            <i class="bx bx-plus me-1"></i>
 | 
					            <i class="bx bx-plus me-1"></i>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -507,13 +507,22 @@ def instance_summary(request, instance_id):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Collect final invoice, payments, and certificate if any
 | 
					    # Collect final invoice, payments, and certificate if any
 | 
				
			||||||
    from invoices.models import Invoice
 | 
					    from invoices.models import Invoice
 | 
				
			||||||
    from installations.models import InstallationReport
 | 
					    from installations.models import InstallationReport, InstallationAssignment
 | 
				
			||||||
    from certificates.models import CertificateInstance
 | 
					    from certificates.models import CertificateInstance
 | 
				
			||||||
    invoice = Invoice.objects.filter(process_instance=instance).first()
 | 
					    invoice = Invoice.objects.filter(process_instance=instance).first()
 | 
				
			||||||
    payments = invoice.payments.filter(is_deleted=False).all() if invoice else []
 | 
					    payments = invoice.payments.filter(is_deleted=False).all() if invoice else []
 | 
				
			||||||
    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
					    latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
 | 
				
			||||||
    certificate = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
 | 
					    certificate = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Calculate installation delay
 | 
				
			||||||
 | 
					    installation_assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
 | 
				
			||||||
 | 
					    installation_delay_days = 0
 | 
				
			||||||
 | 
					    if installation_assignment and latest_report:
 | 
				
			||||||
 | 
					        scheduled_date = installation_assignment.scheduled_date
 | 
				
			||||||
 | 
					        visited_date = latest_report.visited_date
 | 
				
			||||||
 | 
					        if scheduled_date and visited_date and visited_date > scheduled_date:
 | 
				
			||||||
 | 
					            installation_delay_days = (visited_date - scheduled_date).days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Build rows like final invoice step
 | 
					    # Build rows like final invoice step
 | 
				
			||||||
    rows = []
 | 
					    rows = []
 | 
				
			||||||
    if invoice:
 | 
					    if invoice:
 | 
				
			||||||
| 
						 | 
					@ -527,6 +536,8 @@ def instance_summary(request, instance_id):
 | 
				
			||||||
        'rows': rows,
 | 
					        'rows': rows,
 | 
				
			||||||
        'latest_report': latest_report,
 | 
					        'latest_report': latest_report,
 | 
				
			||||||
        'certificate': certificate,
 | 
					        'certificate': certificate,
 | 
				
			||||||
 | 
					        'installation_assignment': installation_assignment,
 | 
				
			||||||
 | 
					        'installation_delay_days': installation_delay_days,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -632,7 +643,7 @@ def export_requests_excel(request):
 | 
				
			||||||
        ).select_related('process_instance')
 | 
					        ).select_related('process_instance')
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        for invoice in invoices:
 | 
					        for invoice in invoices:
 | 
				
			||||||
            if invoice.remaining_amount == 0:  # Fully settled
 | 
					            if invoice.get_remaining_amount() == 0:  # Fully settled
 | 
				
			||||||
                # Find the last payment date for this invoice
 | 
					                # Find the last payment date for this invoice
 | 
				
			||||||
                last_payment = Payment.objects.filter(
 | 
					                last_payment = Payment.objects.filter(
 | 
				
			||||||
                    invoice__process_instance=invoice.process_instance,
 | 
					                    invoice__process_instance=invoice.process_instance,
 | 
				
			||||||
| 
						 | 
					@ -641,24 +652,30 @@ def export_requests_excel(request):
 | 
				
			||||||
                if last_payment:
 | 
					                if last_payment:
 | 
				
			||||||
                    settlement_dates_map[invoice.process_instance_id] = last_payment.created
 | 
					                    settlement_dates_map[invoice.process_instance_id] = last_payment.created
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Get installation approval data
 | 
					        # Get installation approval data by Water Resource Manager role
 | 
				
			||||||
        from processes.models import StepInstance, StepApproval
 | 
					        from processes.models import StepInstance, StepApproval
 | 
				
			||||||
 | 
					        from accounts.models import Role
 | 
				
			||||||
 | 
					        from common.consts import UserRoles
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Get the Water Resource Manager role
 | 
				
			||||||
 | 
					        water_manager_role = Role.objects.filter(slug=UserRoles.WATER_RESOURCE_MANAGER.value).first()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        installation_steps = StepInstance.objects.filter(
 | 
					        installation_steps = StepInstance.objects.filter(
 | 
				
			||||||
            process_instance_id__in=assignment_ids,
 | 
					            process_instance_id__in=assignment_ids,
 | 
				
			||||||
            step__slug='installation_report',  # Assuming this is the slug for installation step
 | 
					            step__order=6,  # Installation report step is order 6
 | 
				
			||||||
            status='completed'
 | 
					            status='completed'
 | 
				
			||||||
        ).select_related('process_instance')
 | 
					        ).select_related('process_instance')
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        for step_instance in installation_steps:
 | 
					        for step_instance in installation_steps:
 | 
				
			||||||
            # Get the approval that completed this step
 | 
					            # Get the approval by Water Resource Manager role that completed this step
 | 
				
			||||||
            approval = StepApproval.objects.filter(
 | 
					            approval = StepApproval.objects.filter(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
                decision='approved',
 | 
					                role=water_manager_role,
 | 
				
			||||||
                is_deleted=False
 | 
					                is_deleted=False
 | 
				
			||||||
            ).select_related('approved_by').order_by('-created').first()
 | 
					            ).select_related('approved_by').order_by('-created_at').first()
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            if approval:
 | 
					            if approval:
 | 
				
			||||||
                approval_dates_map[step_instance.process_instance_id] = approval.created
 | 
					                approval_dates_map[step_instance.process_instance_id] = approval.created_at
 | 
				
			||||||
                approval_users_map[step_instance.process_instance_id] = approval.approved_by
 | 
					                approval_users_map[step_instance.process_instance_id] = approval.approved_by
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Calculate progress and installation data
 | 
					    # Calculate progress and installation data
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,7 @@ id="layout-navbar">
 | 
				
			||||||
 <!-- /Language -->
 | 
					 <!-- /Language -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 <!-- Quick links  -->
 | 
					 <!-- Quick links  -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 <li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0 d-none">
 | 
					 <li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0 d-none">
 | 
				
			||||||
   <a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
 | 
					   <a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
 | 
				
			||||||
      data-bs-auto-close="outside" aria-expanded="false">
 | 
					      data-bs-auto-close="outside" aria-expanded="false">
 | 
				
			||||||
| 
						 | 
					@ -144,6 +145,11 @@ id="layout-navbar">
 | 
				
			||||||
 </li>
 | 
					 </li>
 | 
				
			||||||
 <!-- / Style Switcher-->
 | 
					 <!-- / Style Switcher-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 <li class="nav-item align-items-center">
 | 
				
			||||||
 | 
					  {% if request.user.profile %}
 | 
				
			||||||
 | 
					  <p class="text-muted badge bg-label-primary m-0">{{ request.user.profile.roles_str }}</p>
 | 
				
			||||||
 | 
					  {% endif %}
 | 
				
			||||||
 | 
					</li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 <!-- Notification -->
 | 
					 <!-- Notification -->
 | 
				
			||||||
 <li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none">
 | 
					 <li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,10 +83,12 @@ class WellForm(forms.ModelForm):
 | 
				
			||||||
            'utm_x': forms.NumberInput(attrs={
 | 
					            'utm_x': forms.NumberInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': 'X UTM',
 | 
					                'placeholder': 'X UTM',
 | 
				
			||||||
 | 
					                'required': 'required',
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'utm_y': forms.NumberInput(attrs={
 | 
					            'utm_y': forms.NumberInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
                'placeholder': 'Y UTM',
 | 
					                'placeholder': 'Y UTM',
 | 
				
			||||||
 | 
					                'required': 'required',
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            'utm_zone': forms.NumberInput(attrs={
 | 
					            'utm_zone': forms.NumberInput(attrs={
 | 
				
			||||||
                'class': 'form-control',
 | 
					                'class': 'form-control',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								wells/migrations/0005_alter_historicalwell_utm_x_and_more.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								wells/migrations/0005_alter_historicalwell_utm_x_and_more.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					# Generated by Django 5.2.4 on 2025-10-02 09:32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('wells', '0004_remove_historicalwell_discharge_pipe_diameter_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='utm_x',
 | 
				
			||||||
 | 
					            field=models.DecimalField(decimal_places=0, default=11, max_digits=10, verbose_name='X UTM'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='historicalwell',
 | 
				
			||||||
 | 
					            name='utm_y',
 | 
				
			||||||
 | 
					            field=models.DecimalField(decimal_places=0, default=2, max_digits=10, verbose_name='Y UTM'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='utm_x',
 | 
				
			||||||
 | 
					            field=models.DecimalField(decimal_places=0, max_digits=10, verbose_name='X UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='well',
 | 
				
			||||||
 | 
					            name='utm_y',
 | 
				
			||||||
 | 
					            field=models.DecimalField(decimal_places=0, max_digits=10, verbose_name='Y UTM'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -80,15 +80,11 @@ class Well(SluggedModel):
 | 
				
			||||||
        max_digits=10,
 | 
					        max_digits=10,
 | 
				
			||||||
        decimal_places=0,
 | 
					        decimal_places=0,
 | 
				
			||||||
        verbose_name="X UTM",
 | 
					        verbose_name="X UTM",
 | 
				
			||||||
        null=True,
 | 
					 | 
				
			||||||
        blank=True
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    utm_y = models.DecimalField(
 | 
					    utm_y = models.DecimalField(
 | 
				
			||||||
        max_digits=10,
 | 
					        max_digits=10,
 | 
				
			||||||
        decimal_places=0,
 | 
					        decimal_places=0,
 | 
				
			||||||
        verbose_name="Y UTM",
 | 
					        verbose_name="Y UTM",
 | 
				
			||||||
        null=True,
 | 
					 | 
				
			||||||
        blank=True
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    utm_zone = models.PositiveIntegerField(
 | 
					    utm_zone = models.PositiveIntegerField(
 | 
				
			||||||
        verbose_name="زون UTM",
 | 
					        verbose_name="زون UTM",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue