first commit

This commit is contained in:
aminhashemi92 2025-08-10 07:44:23 +03:30
commit b71ea45681
898 changed files with 138202 additions and 0 deletions

0
invoices/__init__.py Normal file
View file

65
invoices/admin.py Normal file
View file

@ -0,0 +1,65 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import Item, Quote, QuoteItem, Invoice, InvoiceItem, Payment
@admin.register(Item)
class ItemAdmin(SimpleHistoryAdmin):
list_display = ['name', 'unit_price', 'default_quantity', 'is_default_in_quotes', 'is_active', 'created_by']
list_filter = ['is_default_in_quotes', 'is_active', 'created_by']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated']
class QuoteItemInline(admin.TabularInline):
model = QuoteItem
extra = 1
fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
@admin.register(Quote)
class QuoteAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process_instance', 'customer', 'status_display', 'total_amount', 'final_amount', 'valid_until', 'created_by']
list_filter = ['status', 'created', 'valid_until', 'process_instance__process']
search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount']
inlines = [QuoteItemInline]
ordering = ['-created']
def status_display(self, obj):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
class InvoiceItemInline(admin.TabularInline):
model = InvoiceItem
extra = 1
fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
class PaymentInline(admin.TabularInline):
model = Payment
extra = 1
fields = ['amount', 'payment_method', 'reference_number', 'payment_date', 'notes']
readonly_fields = ['created_by', 'created']
@admin.register(Invoice)
class InvoiceAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount', 'remaining_amount', 'due_date']
list_filter = ['status', 'created', 'due_date', 'process_instance__process']
search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount', 'remaining_amount']
inlines = [InvoiceItemInline, PaymentInline]
ordering = ['-created']
def status_display(self, obj):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
@admin.register(Payment)
class PaymentAdmin(SimpleHistoryAdmin):
list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
list_filter = ['payment_method', 'payment_date', 'created_by']
search_fields = ['invoice__name', 'reference_number', 'notes']
readonly_fields = ['created']
ordering = ['-payment_date']

7
invoices/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class InvoicesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'invoices'
verbose_name = 'فاکتورها'

View file

@ -0,0 +1,363 @@
# Generated by Django 5.2.4 on 2025-08-07 09:08
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('processes', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical آیتم',
'verbose_name_plural': 'historical آیتم\u200cها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalQuote',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('valid_until', models.DateField(verbose_name='معتبر تا')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'historical پیش\u200cفاکتور',
'verbose_name_plural': 'historical پیش\u200cفاکتورها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
('due_date', models.DateField(verbose_name='تاریخ سررسید')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invoices', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'فاکتور',
'verbose_name_plural': 'فاکتورها',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='HistoricalPayment',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
],
options={
'verbose_name': 'historical پرداخت',
'verbose_name_plural': 'historical پرداخت\u200cها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
],
options={
'verbose_name': 'آیتم',
'verbose_name_plural': 'آیتم\u200cها',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='InvoiceItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.invoice', verbose_name='فاکتور')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
],
options={
'verbose_name': 'آیتم فاکتور',
'verbose_name_plural': 'آیتم\u200cهای فاکتور',
'ordering': ['invoice', 'item__name'],
},
),
migrations.CreateModel(
name='HistoricalInvoiceItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
],
options={
'verbose_name': 'historical آیتم فاکتور',
'verbose_name_plural': 'historical آیتم\u200cهای فاکتور',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice', verbose_name='فاکتور')),
],
options={
'verbose_name': 'پرداخت',
'verbose_name_plural': 'پرداخت\u200cها',
'ordering': ['-payment_date'],
},
),
migrations.CreateModel(
name='Quote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('valid_until', models.DateField(verbose_name='معتبر تا')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_quotes', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'پیش\u200cفاکتور',
'verbose_name_plural': 'پیش\u200cفاکتورها',
'ordering': ['-created'],
},
),
migrations.AddField(
model_name='invoice',
name='quote',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه'),
),
migrations.CreateModel(
name='HistoricalQuoteItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
],
options={
'verbose_name': 'historical آیتم پیش\u200cفاکتور',
'verbose_name_plural': 'historical آیتم\u200cهای پیش\u200cفاکتور',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalInvoice',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
('due_date', models.DateField(verbose_name='تاریخ سررسید')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه')),
],
options={
'verbose_name': 'historical فاکتور',
'verbose_name_plural': 'historical فاکتورها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='QuoteItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
('quote', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
],
options={
'verbose_name': 'آیتم پیش\u200cفاکتور',
'verbose_name_plural': 'آیتم\u200cهای پیش\u200cفاکتور',
'ordering': ['quote', 'item__name'],
},
),
]

View file

331
invoices/models.py Normal file
View 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()

3
invoices/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
invoices/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.