350 lines
13 KiB
Python
350 lines
13 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
|
|
|
|
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())
|
|
|
|
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
|
|
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)
|
|
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(payment.amount for payment in self.invoice.payments.all())
|
|
self.invoice.paid_amount = total_paid
|
|
self.invoice.calculate_totals()
|