fix final invoice and certificatre
This commit is contained in:
		
							parent
							
								
									0cfa86bde3
								
							
						
					
					
						commit
						dd37ac3720
					
				
					 11 changed files with 120 additions and 66 deletions
				
			
		| 
						 | 
					@ -78,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.
										
									
								
							| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -942,8 +942,8 @@ 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').all())
 | 
					    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}
 | 
				
			||||||
    rejections_by_role = {r.role_id: r for r in rejections}
 | 
					    rejections_by_role = {r.role_id: r for r in rejections}
 | 
				
			||||||
    approver_statuses = []
 | 
					    approver_statuses = []
 | 
				
			||||||
| 
						 | 
					@ -978,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
 | 
				
			||||||
| 
						 | 
					@ -997,8 +997,8 @@ 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.create(
 | 
					            StepApproval.objects.create(
 | 
				
			||||||
                step_instance=step_instance,
 | 
					                step_instance=step_instance,
 | 
				
			||||||
| 
						 | 
					@ -1203,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()),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1216,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:
 | 
				
			||||||
| 
						 | 
					@ -1231,18 +1234,13 @@ 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:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
        for rej in list(si.rejections.all()):
 | 
					        for rej in list(si.rejections.all()):
 | 
				
			||||||
            rej.delete()
 | 
					            rej.delete()
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
    except Exception:
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Reset ALL subsequent completed steps to in_progress
 | 
					    # Reset ALL subsequent completed steps to in_progress
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -1274,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()),
 | 
				
			||||||
    }})
 | 
					    }})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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">
 | 
				
			||||||
| 
						 | 
					@ -256,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 %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -643,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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue