first commit
This commit is contained in:
		
						commit
						b71ea45681
					
				
					 898 changed files with 138202 additions and 0 deletions
				
			
		
							
								
								
									
										331
									
								
								invoices/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								invoices/models.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,331 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from common.models import NameSlugModel, BaseModel
 | 
			
		||||
from simple_history.models import HistoricalRecords
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
User = get_user_model()
 | 
			
		||||
 | 
			
		||||
class Item(NameSlugModel):
 | 
			
		||||
    """مدل آیتمهای پیشفرض"""
 | 
			
		||||
    description = models.TextField(verbose_name="توضیحات", blank=True)
 | 
			
		||||
    unit_price = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        verbose_name="قیمت واحد"
 | 
			
		||||
    )
 | 
			
		||||
    default_quantity = models.PositiveIntegerField(
 | 
			
		||||
        default=1, 
 | 
			
		||||
        verbose_name="تعداد پیشفرض"
 | 
			
		||||
    )
 | 
			
		||||
    is_default_in_quotes = models.BooleanField(
 | 
			
		||||
        default=False, 
 | 
			
		||||
        verbose_name="پیشفرض در پیشفاکتورها",
 | 
			
		||||
        help_text="این آیتم به صورت پیشفرض در همه پیشفاکتورها قرار میگیرد"
 | 
			
		||||
    )
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم"
 | 
			
		||||
        verbose_name_plural = "آیتمها"
 | 
			
		||||
        ordering = ['name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.name} - {self.unit_price} تومان"
 | 
			
		||||
 | 
			
		||||
class Quote(NameSlugModel):
 | 
			
		||||
    """مدل پیشفاکتور"""
 | 
			
		||||
    process_instance = models.ForeignKey(
 | 
			
		||||
        'processes.ProcessInstance', 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="نمونه فرآیند"
 | 
			
		||||
    )
 | 
			
		||||
    customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('draft', 'پیشنویس'),
 | 
			
		||||
            ('sent', 'ارسال شده'),
 | 
			
		||||
            ('accepted', 'تایید شده'),
 | 
			
		||||
            ('rejected', 'رد شده'),
 | 
			
		||||
            ('expired', 'منقضی شده'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='draft',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    total_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ کل"
 | 
			
		||||
    )
 | 
			
		||||
    discount_percent = models.DecimalField(
 | 
			
		||||
        max_digits=5, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="درصد تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    discount_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    final_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ نهایی"
 | 
			
		||||
    )
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    valid_until = models.DateField(verbose_name="معتبر تا")
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        related_name='created_quotes'
 | 
			
		||||
    )
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "پیشفاکتور"
 | 
			
		||||
        verbose_name_plural = "پیشفاکتورها"
 | 
			
		||||
        ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"پیشفاکتور {self.name} - {self.customer.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def calculate_totals(self):
 | 
			
		||||
        """محاسبه مبالغ کل"""
 | 
			
		||||
        total = sum(item.total_price for item in self.items.all())
 | 
			
		||||
        self.total_amount = total
 | 
			
		||||
        
 | 
			
		||||
        # محاسبه تخفیف
 | 
			
		||||
        if self.discount_percent > 0:
 | 
			
		||||
            self.discount_amount = (total * self.discount_percent) / 100
 | 
			
		||||
        else:
 | 
			
		||||
            self.discount_amount = 0
 | 
			
		||||
        
 | 
			
		||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
        """نمایش وضعیت با رنگ"""
 | 
			
		||||
        status_colors = {
 | 
			
		||||
            'draft': 'secondary',
 | 
			
		||||
            'sent': 'primary',
 | 
			
		||||
            'accepted': 'success',
 | 
			
		||||
            'rejected': 'danger',
 | 
			
		||||
            'expired': 'warning',
 | 
			
		||||
        }
 | 
			
		||||
        color = status_colors.get(self.status, 'secondary')
 | 
			
		||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
			
		||||
 | 
			
		||||
class QuoteItem(BaseModel):
 | 
			
		||||
    """مدل آیتمهای پیشفاکتور"""
 | 
			
		||||
    quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیشفاکتور")
 | 
			
		||||
    item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
 | 
			
		||||
    quantity = models.PositiveIntegerField(verbose_name="تعداد")
 | 
			
		||||
    unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
 | 
			
		||||
    total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم پیشفاکتور"
 | 
			
		||||
        verbose_name_plural = "آیتمهای پیشفاکتور"
 | 
			
		||||
        ordering = ['quote', 'item__name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.item.name} - {self.quantity} عدد"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """محاسبه قیمت کل"""
 | 
			
		||||
        self.total_price = self.quantity * self.unit_price
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبالغ پیشفاکتور
 | 
			
		||||
        self.quote.calculate_totals()
 | 
			
		||||
 | 
			
		||||
class Invoice(NameSlugModel):
 | 
			
		||||
    """مدل فاکتور نهایی"""
 | 
			
		||||
    quote = models.ForeignKey(
 | 
			
		||||
        Quote, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="پیشفاکتور مربوطه",
 | 
			
		||||
        null=True, 
 | 
			
		||||
        blank=True
 | 
			
		||||
    )
 | 
			
		||||
    process_instance = models.ForeignKey(
 | 
			
		||||
        'processes.ProcessInstance', 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="نمونه فرآیند"
 | 
			
		||||
    )
 | 
			
		||||
    customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('draft', 'پیشنویس'),
 | 
			
		||||
            ('sent', 'ارسال شده'),
 | 
			
		||||
            ('paid', 'پرداخت شده'),
 | 
			
		||||
            ('partially_paid', 'نیمه پرداخت شده'),
 | 
			
		||||
            ('overdue', 'معوق'),
 | 
			
		||||
            ('cancelled', 'لغو شده'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='draft',
 | 
			
		||||
        verbose_name="وضعیت"
 | 
			
		||||
    )
 | 
			
		||||
    total_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ کل"
 | 
			
		||||
    )
 | 
			
		||||
    discount_percent = models.DecimalField(
 | 
			
		||||
        max_digits=5, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="درصد تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    discount_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        verbose_name="مبلغ تخفیف"
 | 
			
		||||
    )
 | 
			
		||||
    final_amount = models.DecimalField(
 | 
			
		||||
        max_digits=15, 
 | 
			
		||||
        decimal_places=2, 
 | 
			
		||||
        default=0, 
 | 
			
		||||
        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="تاریخ سررسید")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    created_by = models.ForeignKey(
 | 
			
		||||
        User, 
 | 
			
		||||
        on_delete=models.CASCADE, 
 | 
			
		||||
        verbose_name="ایجاد کننده",
 | 
			
		||||
        related_name='created_invoices'
 | 
			
		||||
    )
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "فاکتور"
 | 
			
		||||
        verbose_name_plural = "فاکتورها"
 | 
			
		||||
        ordering = ['-created']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"فاکتور {self.name} - {self.customer.get_full_name()}"
 | 
			
		||||
 | 
			
		||||
    def calculate_totals(self):
 | 
			
		||||
        """محاسبه مبالغ کل"""
 | 
			
		||||
        total = sum(item.total_price for item in self.items.all())
 | 
			
		||||
        self.total_amount = total
 | 
			
		||||
        
 | 
			
		||||
        # محاسبه تخفیف
 | 
			
		||||
        if self.discount_percent > 0:
 | 
			
		||||
            self.discount_amount = (total * self.discount_percent) / 100
 | 
			
		||||
        else:
 | 
			
		||||
            self.discount_amount = 0
 | 
			
		||||
        
 | 
			
		||||
        self.final_amount = self.total_amount - self.discount_amount
 | 
			
		||||
        self.remaining_amount = self.final_amount - self.paid_amount
 | 
			
		||||
        
 | 
			
		||||
        # بروزرسانی وضعیت
 | 
			
		||||
        if self.remaining_amount <= 0:
 | 
			
		||||
            self.status = 'paid'
 | 
			
		||||
        elif self.paid_amount > 0:
 | 
			
		||||
            self.status = 'partially_paid'
 | 
			
		||||
        else:
 | 
			
		||||
            self.status = 'sent'
 | 
			
		||||
        
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def get_status_display_with_color(self):
 | 
			
		||||
        """نمایش وضعیت با رنگ"""
 | 
			
		||||
        status_colors = {
 | 
			
		||||
            'draft': 'secondary',
 | 
			
		||||
            'sent': 'primary',
 | 
			
		||||
            'paid': 'success',
 | 
			
		||||
            'partially_paid': 'info',
 | 
			
		||||
            'overdue': 'danger',
 | 
			
		||||
            'cancelled': 'warning',
 | 
			
		||||
        }
 | 
			
		||||
        color = status_colors.get(self.status, 'secondary')
 | 
			
		||||
        return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
 | 
			
		||||
 | 
			
		||||
class InvoiceItem(BaseModel):
 | 
			
		||||
    """مدل آیتمهای فاکتور"""
 | 
			
		||||
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='items', verbose_name="فاکتور")
 | 
			
		||||
    item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
 | 
			
		||||
    quantity = models.PositiveIntegerField(verbose_name="تعداد")
 | 
			
		||||
    unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
 | 
			
		||||
    total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "آیتم فاکتور"
 | 
			
		||||
        verbose_name_plural = "آیتمهای فاکتور"
 | 
			
		||||
        ordering = ['invoice', 'item__name']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.item.name} - {self.quantity} عدد"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """محاسبه قیمت کل"""
 | 
			
		||||
        self.total_price = self.quantity * self.unit_price
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبالغ فاکتور
 | 
			
		||||
        self.invoice.calculate_totals()
 | 
			
		||||
 | 
			
		||||
class Payment(BaseModel):
 | 
			
		||||
    """مدل پرداختها"""
 | 
			
		||||
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', verbose_name="فاکتور")
 | 
			
		||||
    amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="مبلغ پرداخت")
 | 
			
		||||
    payment_method = models.CharField(
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=[
 | 
			
		||||
            ('cash', 'نقدی'),
 | 
			
		||||
            ('bank_transfer', 'انتقال بانکی'),
 | 
			
		||||
            ('check', 'چک'),
 | 
			
		||||
            ('card', 'کارت بانکی'),
 | 
			
		||||
            ('other', 'سایر'),
 | 
			
		||||
        ],
 | 
			
		||||
        default='cash',
 | 
			
		||||
        verbose_name="روش پرداخت"
 | 
			
		||||
    )
 | 
			
		||||
    reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True)
 | 
			
		||||
    payment_date = models.DateField(verbose_name="تاریخ پرداخت")
 | 
			
		||||
    notes = models.TextField(verbose_name="یادداشتها", blank=True)
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ثبت کننده")
 | 
			
		||||
    history = HistoricalRecords()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = "پرداخت"
 | 
			
		||||
        verbose_name_plural = "پرداختها"
 | 
			
		||||
        ordering = ['-payment_date']
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"پرداخت {self.amount} تومان - {self.invoice.name}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        """بروزرسانی مبالغ فاکتور"""
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
        # بروزرسانی مبلغ پرداخت شده فاکتور
 | 
			
		||||
        total_paid = sum(payment.amount for payment in self.invoice.payments.all())
 | 
			
		||||
        self.invoice.paid_amount = total_paid
 | 
			
		||||
        self.invoice.calculate_totals()
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue