diff --git a/_base/settings.py b/_base/settings.py index 8261409..e9079ca 100644 --- a/_base/settings.py +++ b/_base/settings.py @@ -173,3 +173,6 @@ JAZZMIN_SETTINGS = { "custom_js": None, } + +# VAT / Value Added Tax percent (e.g., 0.09 for 9%) +VAT_RATE = 0.1 \ No newline at end of file diff --git a/_helpers/utils.py b/_helpers/utils.py index e4ee804..a7adccb 100644 --- a/_helpers/utils.py +++ b/_helpers/utils.py @@ -144,7 +144,7 @@ def persian_converter2(time): def persian_converter3(time): - time = time + datetime.timedelta(days=1) + time = time time_to_str = "{},{},{}".format(time.year, time.month, time.day) time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple() time_to_list = list(time_to_tuple) diff --git a/accounts/admin.py b/accounts/admin.py index d741357..7ffe250 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -16,6 +16,8 @@ class ProfileAdmin(admin.ModelAdmin): list_display = [ "user", "fullname", + "user_type_display", + "company_name", "pic_tag", "roles_str", "affairs", @@ -25,8 +27,52 @@ class ProfileAdmin(admin.ModelAdmin): "is_active", "jcreated", ] - search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number'] - list_filter = ['user', 'roles', 'affairs', 'county', 'broker'] + search_fields = [ + 'user__username', + 'user__first_name', + 'user__last_name', + 'user__phone_number', + 'company_name', + 'company_national_id', + 'national_code' + ] + list_filter = [ + 'user_type', + 'user', + 'roles', + 'affairs', + 'county', + 'broker', + 'is_completed', + 'is_active' + ] + fieldsets = ( + ('اطلاعات کاربری', { + 'fields': ('user', 'user_type', 'pic', 'roles') + }), + ('اطلاعات شخصی - حقیقی', { + 'fields': ('national_code', 'address', 'phone_number_1', 'phone_number_2'), + 'classes': ('collapse',), + }), + ('اطلاعات شرکت - حقوقی', { + 'fields': ('company_name', 'company_national_id'), + 'classes': ('collapse',), + }), + ('اطلاعات بانکی', { + 'fields': ('card_number', 'account_number', 'bank_name'), + 'classes': ('collapse',), + }), + ('اطلاعات سازمانی', { + 'fields': ('affairs', 'county', 'broker', 'owner'), + }), + ('وضعیت', { + 'fields': ('is_completed', 'is_active'), + }), + ('تاریخها', { + 'fields': ('created', 'updated'), + 'classes': ('collapse',), + }), + ) date_hierarchy = 'created' ordering = ['-created'] readonly_fields = ['created', 'updated'] diff --git a/accounts/forms.py b/accounts/forms.py index 76beb31..a5d493b 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -2,7 +2,7 @@ from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.forms import UserCreationForm from .models import Profile, Role -from common.consts import UserRoles +from common.consts import UserRoles, USER_TYPE_CHOICES User = get_user_model() @@ -28,10 +28,15 @@ class CustomerForm(forms.ModelForm): class Meta: model = Profile fields = [ - 'phone_number_1', 'phone_number_2', 'national_code', + 'user_type', 'phone_number_1', 'phone_number_2', 'national_code', + 'company_name', 'company_national_id', 'address', 'card_number', 'account_number', 'bank_name' ] widgets = { + 'user_type': forms.Select(attrs={ + 'class': 'form-control', + 'id': 'user-type-select' + }), 'phone_number_1': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '09123456789' @@ -46,6 +51,15 @@ class CustomerForm(forms.ModelForm): 'maxlength': '10', 'required': 'required' }), + 'company_name': forms.TextInput(attrs={ + 'class': 'form-control company-field', + 'placeholder': 'نام شرکت' + }), + 'company_national_id': forms.TextInput(attrs={ + 'class': 'form-control company-field', + 'placeholder': 'شناسه ملی شرکت', + 'maxlength': '11' + }), 'address': forms.Textarea(attrs={ 'class': 'form-control', 'placeholder': 'آدرس کامل', @@ -67,9 +81,12 @@ class CustomerForm(forms.ModelForm): }), } labels = { + 'user_type': 'نوع کاربر', 'phone_number_1': 'تلفن ۱', 'phone_number_2': 'تلفن ۲', 'national_code': 'کد ملی', + 'company_name': 'نام شرکت', + 'company_national_id': 'شناسه ملی شرکت', 'address': 'آدرس', 'card_number': 'شماره کارت', 'account_number': 'شماره حساب', @@ -89,6 +106,21 @@ class CustomerForm(forms.ModelForm): raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.') return national_code + def clean(self): + cleaned_data = super().clean() + user_type = cleaned_data.get('user_type') + company_name = cleaned_data.get('company_name') + company_national_id = cleaned_data.get('company_national_id') + + # If user type is legal, company fields are required + if user_type == 'legal': + if not company_name: + self.add_error('company_name', 'برای کاربران حقوقی نام شرکت الزامی است.') + if not company_national_id: + self.add_error('company_national_id', 'برای کاربران حقوقی شناسه ملی شرکت الزامی است.') + + return cleaned_data + def save(self, commit=True): def _compute_completed(cleaned): try: @@ -100,7 +132,15 @@ class CustomerForm(forms.ModelForm): bank_ok = bool(cleaned.get('bank_name')) card_ok = bool((cleaned.get('card_number') or '').strip()) acc_ok = bool((cleaned.get('account_number') or '').strip()) - return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok]) + + # Check user type specific requirements + user_type = cleaned.get('user_type', 'individual') + if user_type == 'legal': + company_name_ok = bool((cleaned.get('company_name') or '').strip()) + company_id_ok = bool((cleaned.get('company_national_id') or '').strip()) + return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok, company_name_ok, company_id_ok]) + else: + return all([first_ok, last_ok, nc_ok, phone_ok, addr_ok, bank_ok, card_ok, acc_ok]) except Exception: return False # Check if this is an update (instance exists) @@ -125,9 +165,12 @@ class CustomerForm(forms.ModelForm): profile.is_completed = _compute_completed({ 'first_name': user.first_name, 'last_name': user.last_name, + 'user_type': self.cleaned_data.get('user_type'), 'national_code': self.cleaned_data.get('national_code'), 'phone_number_1': self.cleaned_data.get('phone_number_1'), 'phone_number_2': self.cleaned_data.get('phone_number_2'), + 'company_name': self.cleaned_data.get('company_name'), + 'company_national_id': self.cleaned_data.get('company_national_id'), 'address': self.cleaned_data.get('address'), 'bank_name': self.cleaned_data.get('bank_name'), 'card_number': self.cleaned_data.get('card_number'), @@ -171,9 +214,12 @@ class CustomerForm(forms.ModelForm): profile.is_completed = _compute_completed({ 'first_name': user.first_name, 'last_name': user.last_name, + 'user_type': self.cleaned_data.get('user_type'), 'national_code': self.cleaned_data.get('national_code'), 'phone_number_1': self.cleaned_data.get('phone_number_1'), 'phone_number_2': self.cleaned_data.get('phone_number_2'), + 'company_name': self.cleaned_data.get('company_name'), + 'company_national_id': self.cleaned_data.get('company_national_id'), 'address': self.cleaned_data.get('address'), 'bank_name': self.cleaned_data.get('bank_name'), 'card_number': self.cleaned_data.get('card_number'), diff --git a/accounts/migrations/0007_historicalprofile_company_name_and_more.py b/accounts/migrations/0007_historicalprofile_company_name_and_more.py new file mode 100644 index 0000000..0d2edc1 --- /dev/null +++ b/accounts/migrations/0007_historicalprofile_company_name_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.4 on 2025-09-21 07:37 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_company_card_holder_name'), + ] + + operations = [ + migrations.AddField( + model_name='historicalprofile', + name='company_name', + field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=255, null=True, verbose_name='نام شرکت'), + ), + migrations.AddField( + model_name='historicalprofile', + name='company_national_id', + field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=11, null=True, validators=[django.core.validators.RegexValidator(code='invalid_company_national_id', message='شناسه ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شناسه ملی شرکت'), + ), + migrations.AddField( + model_name='historicalprofile', + name='user_type', + field=models.CharField(choices=[('individual', 'حقیقی'), ('legal', 'حقوقی')], default='individual', max_length=20, verbose_name='نوع کاربر'), + ), + migrations.AddField( + model_name='profile', + name='company_name', + field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=255, null=True, verbose_name='نام شرکت'), + ), + migrations.AddField( + model_name='profile', + name='company_national_id', + field=models.CharField(blank=True, help_text='فقط برای کاربران حقوقی الزامی است', max_length=11, null=True, validators=[django.core.validators.RegexValidator(code='invalid_company_national_id', message='شناسه ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شناسه ملی شرکت'), + ), + migrations.AddField( + model_name='profile', + name='user_type', + field=models.CharField(choices=[('individual', 'حقیقی'), ('legal', 'حقوقی')], default='individual', max_length=20, verbose_name='نوع کاربر'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 937c329..348304e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -4,7 +4,7 @@ from django.utils.html import format_html from django.core.validators import RegexValidator from simple_history.models import HistoricalRecords from common.models import TagModel, BaseModel, NameSlugModel -from common.consts import UserRoles, BANK_CHOICES +from common.consts import UserRoles, BANK_CHOICES, USER_TYPE_CHOICES from locations.models import Affairs, Broker, County @@ -88,6 +88,33 @@ class Profile(BaseModel): verbose_name="شماره تماس ۲", blank=True ) + user_type = models.CharField( + max_length=20, + choices=USER_TYPE_CHOICES, + default='individual', + verbose_name="نوع کاربر" + ) + company_national_id = models.CharField( + max_length=11, + null=True, + verbose_name="شناسه ملی شرکت", + blank=True, + validators=[ + RegexValidator( + regex=r'^\d+$', + message='شناسه ملی باید فقط شامل اعداد باشد.', + code='invalid_company_national_id' + ) + ], + help_text="فقط برای کاربران حقوقی الزامی است" + ) + company_name = models.CharField( + max_length=255, + null=True, + verbose_name="نام شرکت", + blank=True, + help_text="فقط برای کاربران حقوقی الزامی است" + ) pic = models.ImageField( upload_to="profile_images", @@ -179,6 +206,23 @@ class Profile(BaseModel): pic_tag.short_description = "تصویر" + def is_legal_entity(self): + return self.user_type == 'legal' + + def is_individual(self): + return self.user_type == 'individual' + + def get_display_name(self): + """Returns appropriate display name based on user type""" + if self.is_legal_entity() and self.company_name: + return self.company_name + return self.user.get_full_name() or str(self.user) + + def user_type_display(self): + return dict(USER_TYPE_CHOICES).get(self.user_type, self.user_type) + + user_type_display.short_description = "نوع کاربر" + class Company(NameSlugModel): logo = models.ImageField( diff --git a/accounts/templates/accounts/customer_list.html b/accounts/templates/accounts/customer_list.html index 24f5e5a..c1e2c0d 100644 --- a/accounts/templates/accounts/customer_list.html +++ b/accounts/templates/accounts/customer_list.html @@ -61,6 +61,7 @@