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 from django.utils import timezone from django.core.validators import MinValueValidator from django.conf import settings 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="قیمت واحد" ) is_special = models.BooleanField(default=False, 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.filter(is_deleted=False).all()) total = sum(item.total_price for item in self.items.filter(is_deleted=False).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 '{}'.format(color, self.get_status_display()) def get_paid_amount(self): """مبلغ پرداخت شده برای این پیش‌فاکتور بر اساس پرداخت‌های فاکتور مرتبط""" invoice = Invoice.objects.filter(quote=self).first() if not invoice: return Decimal('0') return sum(p.amount for p in invoice.payments.filter(is_deleted=False).all()) def get_remaining_amount(self): """مبلغ باقی‌مانده بر اساس پرداخت‌ها""" paid = self.get_paid_amount() remaining = self.final_amount - paid if remaining < 0: remaining = Decimal('0') return remaining 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 # خالص مانده به نفع شرکت (مثبت) یا به نفع مشتری (منفی) net_due = self.final_amount - self.paid_amount self.remaining_amount = net_due # وضعیت بر اساس مانده خالص if net_due == 0: self.status = 'paid' elif net_due > 0: # مشتری هنوز باید پرداخت کند self.status = 'partially_paid' if self.paid_amount > 0 else 'sent' else: # شرکت باید به مشتری پرداخت کند self.status = 'partially_paid' 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 '{}'.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="مبلغ پرداخت") direction = models.CharField( max_length=3, choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', 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, unique=True) payment_date = models.DateField(verbose_name="تاریخ پرداخت") notes = models.TextField(verbose_name="یادداشت‌ها", blank=True) receipt_image = models.ImageField(upload_to='payments/%Y/%m/%d/', null=True, blank=True, verbose_name="تصویر فیش") 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((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() def delete(self, using=None, keep_parents=False): """حذف نرم و بروزرسانی مبالغ فاکتور پس از حذف""" result = super().delete(using=using, keep_parents=keep_parents) 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() except Exception: pass return result