shafafiyat/invoices/models.py

378 lines
14 KiB
Python

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
from _helpers.utils import jalali_converter2
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 '<span class="badge bg-{}">{}</span>'.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 '<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="مبلغ پرداخت")
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
def jpayment_date(self):
return jalali_converter2(self.payment_date)