diff --git a/accounts/admin.py b/accounts/admin.py index 1d58e16..d741357 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -33,9 +33,9 @@ class ProfileAdmin(admin.ModelAdmin): @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): - list_display = ['name', 'logo', 'signature', 'address', 'phone'] + list_display = ['name', 'logo', 'signature', 'address', 'phone', 'broker', 'registration_number'] prepopulated_fields = {'slug': ('name',)} search_fields = ['name', 'address', 'phone'] - list_filter = ['is_active'] + list_filter = ['is_active', 'broker'] date_hierarchy = 'created' ordering = ['-created'] \ No newline at end of file diff --git a/accounts/forms.py b/accounts/forms.py index 7654a13..76beb31 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -90,6 +90,19 @@ class CustomerForm(forms.ModelForm): return national_code def save(self, commit=True): + def _compute_completed(cleaned): + try: + first_ok = bool((cleaned.get('first_name') or '').strip()) + last_ok = bool((cleaned.get('last_name') or '').strip()) + nc_ok = bool((cleaned.get('national_code') or '').strip()) + phone_ok = bool((cleaned.get('phone_number_1') or '').strip() or (cleaned.get('phone_number_2') or '').strip()) + addr_ok = bool((cleaned.get('address') or '').strip()) + 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]) + except Exception: + return False # Check if this is an update (instance exists) if self.instance and self.instance.pk: # Update existing profile @@ -108,6 +121,18 @@ class CustomerForm(forms.ModelForm): profile.affairs = current_user_profile.affairs profile.county = current_user_profile.county profile.broker = current_user_profile.broker + # Set completion flag based on provided form data + profile.is_completed = _compute_completed({ + 'first_name': user.first_name, + 'last_name': user.last_name, + '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'), + 'address': self.cleaned_data.get('address'), + 'bank_name': self.cleaned_data.get('bank_name'), + 'card_number': self.cleaned_data.get('card_number'), + 'account_number': self.cleaned_data.get('account_number'), + }) if commit: profile.save() @@ -142,6 +167,18 @@ class CustomerForm(forms.ModelForm): profile.affairs = current_user_profile.affairs profile.county = current_user_profile.county profile.broker = current_user_profile.broker + # Set completion flag based on provided form data + profile.is_completed = _compute_completed({ + 'first_name': user.first_name, + 'last_name': user.last_name, + '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'), + 'address': self.cleaned_data.get('address'), + 'bank_name': self.cleaned_data.get('bank_name'), + 'card_number': self.cleaned_data.get('card_number'), + 'account_number': self.cleaned_data.get('account_number'), + }) if commit: profile.save() diff --git a/accounts/migrations/0002_company_broker.py b/accounts/migrations/0002_company_broker.py new file mode 100644 index 0000000..df12560 --- /dev/null +++ b/accounts/migrations/0002_company_broker.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.4 on 2025-09-07 13:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ('locations', '0003_remove_broker_company'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='broker', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='company', to='locations.broker', verbose_name='کارگزار'), + ), + ] diff --git a/accounts/migrations/0003_company_account_number_company_bank_name_and_more.py b/accounts/migrations/0003_company_account_number_company_bank_name_and_more.py new file mode 100644 index 0000000..6e692ff --- /dev/null +++ b/accounts/migrations/0003_company_account_number_company_bank_name_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.4 on 2025-09-07 14:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_company_broker'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='account_number', + field=models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب'), + ), + migrations.AddField( + model_name='company', + name='bank_name', + field=models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک'), + ), + migrations.AddField( + model_name='company', + name='card_number', + field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت'), + ), + migrations.AddField( + model_name='company', + name='sheba_number', + field=models.CharField(blank=True, max_length=30, null=True, verbose_name='شماره شبا'), + ), + ] diff --git a/accounts/migrations/0004_company_branch_name.py b/accounts/migrations/0004_company_branch_name.py new file mode 100644 index 0000000..25114e1 --- /dev/null +++ b/accounts/migrations/0004_company_branch_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-09-07 14:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_company_account_number_company_bank_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='branch_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شعبه بانک'), + ), + ] diff --git a/accounts/migrations/0005_company_registration_number.py b/accounts/migrations/0005_company_registration_number.py new file mode 100644 index 0000000..b38ab10 --- /dev/null +++ b/accounts/migrations/0005_company_registration_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-09-08 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_company_branch_name'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='registration_number', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شماره ثبت شرکت'), + ), + ] diff --git a/accounts/migrations/0006_company_card_holder_name.py b/accounts/migrations/0006_company_card_holder_name.py new file mode 100644 index 0000000..9228034 --- /dev/null +++ b/accounts/migrations/0006_company_card_holder_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-09-08 10:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_company_registration_number'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='card_holder_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='نام دارنده کارت'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index a94311a..937c329 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -181,11 +181,94 @@ class Profile(BaseModel): class Company(NameSlugModel): - logo = models.ImageField(upload_to='companies/logos', null=True, blank=True, verbose_name='لوگوی شرکت') - signature = models.ImageField(upload_to='companies/signatures', null=True, blank=True, verbose_name='امضای شرکت') - address = models.TextField(null=True, blank=True, verbose_name='آدرس') - phone = models.CharField(max_length=11, null=True, blank=True, verbose_name='شماره تماس') - + logo = models.ImageField( + upload_to='companies/logos', + null=True, + blank=True, + verbose_name='لوگوی شرکت' + ) + signature = models.ImageField( + upload_to='companies/signatures', + null=True, + blank=True, + verbose_name='امضای شرکت' + ) + address = models.TextField( + null=True, + blank=True, + verbose_name='آدرس' + ) + phone = models.CharField( + max_length=11, + null=True, + blank=True, + verbose_name='شماره تماس' + ) + registration_number = models.CharField( + max_length=255, + null=True, + blank=True, + verbose_name='شماره ثبت شرکت' + ) + broker = models.OneToOneField( + Broker, + on_delete=models.SET_NULL, + verbose_name="کارگزار", + null=True, + blank=True, + related_name='company' + ) + card_number = models.CharField( + max_length=16, + null=True, + verbose_name="شماره کارت", + blank=True, + validators=[ + RegexValidator( + regex=r'^\d+$', + message='شماره کارت باید فقط شامل اعداد باشد.', + code='invalid_card_number' + ) + ] + ) + account_number = models.CharField( + max_length=20, + null=True, + verbose_name="شماره حساب", + blank=True, + validators=[ + RegexValidator( + regex=r'^\d+$', + message='شماره حساب باید فقط شامل اعداد باشد.', + code='invalid_account_number' + ) + ] + ) + card_holder_name = models.CharField( + max_length=255, + null=True, + verbose_name="نام دارنده کارت", + blank=True, + ) + sheba_number = models.CharField( + max_length=30, + null=True, + verbose_name="شماره شبا", + blank=True, + ) + bank_name = models.CharField( + max_length=255, + choices=BANK_CHOICES, + null=True, + verbose_name="نام بانک", + blank=True + ) + branch_name = models.CharField( + max_length=255, + null=True, + verbose_name="شعبه بانک", + blank=True + ) class Meta: verbose_name = 'شرکت' verbose_name_plural = 'شرکت‌ها' diff --git a/accounts/templates/accounts/customer_list.html b/accounts/templates/accounts/customer_list.html index 2d00356..1b7e103 100644 --- a/accounts/templates/accounts/customer_list.html +++ b/accounts/templates/accounts/customer_list.html @@ -172,12 +172,19 @@ {% empty %} - +
هیچ کاربری یافت نشد
+ + + + + + + {% endfor %} diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html index e5791f3..e43800d 100644 --- a/accounts/templates/accounts/login.html +++ b/accounts/templates/accounts/login.html @@ -16,6 +16,9 @@ layout-wide customizer-hide {% endblock style %} {% block content %} + +{% include '_toasts.html' %} +
@@ -69,7 +72,7 @@ layout-wide customizer-hide {% csrf_token %}
- +
diff --git a/accounts/urls.py b/accounts/urls.py index ac5119d..7c79207 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -4,7 +4,7 @@ from accounts.views import login_view, dashboard, customer_list, add_customer_aj app_name = "accounts" urlpatterns = [ - path('login/', login_view, name='login'), + path('', login_view, name='login'), path('logout/', logout_view, name='logout'), path('dashboard/', dashboard, name='dashboard'), path('customers/', customer_list, name='customer_list'), diff --git a/accounts/views.py b/accounts/views.py index 6a4f23c..c5bec75 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -8,7 +8,9 @@ from django import forms from django.contrib.auth.decorators import login_required from accounts.models import Profile from accounts.forms import CustomerForm +from processes.utils import scope_customers_queryset from common.consts import UserRoles +from common.decorators import allowed_roles # Create your views here. @@ -17,6 +19,9 @@ def login_view(request): renders login page and authenticating user POST requests to log user in """ + # If already authenticated, go straight to request list + if request.user.is_authenticated: + return redirect("processes:request_list") if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") @@ -35,9 +40,11 @@ def dashboard(request): @login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def customer_list(request): # Get all profiles that have customer role - customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user') + base = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user') + customers = scope_customers_queryset(request.user, base) form = CustomerForm() return render(request, "accounts/customer_list.html", { @@ -47,6 +54,8 @@ def customer_list(request): @require_POST +@login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def add_customer_ajax(request): """AJAX endpoint for adding customers""" form = CustomerForm(request.POST, request.FILES) @@ -85,6 +94,8 @@ def add_customer_ajax(request): @require_POST +@login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def edit_customer_ajax(request, customer_id): customer = get_object_or_404(Profile, id=customer_id) form = CustomerForm(request.POST, request.FILES, instance=customer) @@ -122,6 +133,7 @@ def edit_customer_ajax(request, customer_id): }) @require_GET +@login_required def get_customer_data(request, customer_id): customer = get_object_or_404(Profile, id=customer_id) @@ -162,6 +174,7 @@ def get_customer_data(request, customer_id): }) +@login_required def logout_view(request): """Log out current user and redirect to login page.""" logout(request) diff --git a/certificates/models.py b/certificates/models.py index 64d53f9..1b3dcaf 100644 --- a/certificates/models.py +++ b/certificates/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth import get_user_model from common.models import BaseModel +from _helpers.utils import jalali_converter2 User = get_user_model() @@ -35,4 +36,7 @@ class CertificateInstance(BaseModel): def __str__(self): return f"گواهی {self.process_instance.code}" + def jissued_at(self): + return jalali_converter2(self.issued_at) + diff --git a/certificates/templates/certificates/print.html b/certificates/templates/certificates/print.html index 5bd26c2..d5ef11f 100644 --- a/certificates/templates/certificates/print.html +++ b/certificates/templates/certificates/print.html @@ -1,28 +1,71 @@ -{% extends '_base.html' %} + + + + + + تاییدیه - {{ instance.code }} + {% load static %} -{% block content %} -
-
- {% if template.company and template.company.logo %} - logo - {% endif %} -

{{ cert.rendered_title }}

- {% if template.company %}
{{ template.company.name }}
{% endif %} -
-
- {{ cert.rendered_body|safe }} -
-
-
تاریخ: {{ cert.issued_at }}
-
- {% if template.company and template.company.signature %} - seal + + + + + + + + + + + + + + +
+ +
+
+
شماره درخواست: {{ instance.code }}
+
تاریخ: {{ cert.jissued_at }}
+
+
+ + +
+ {% if template.company and template.company.logo %} + logo {% endif %} -
مهر و امضای شرکت
+

{{ cert.rendered_title }}

+ {% if template.company %} +
{{ template.company.name }}
+ {% endif %} +
+ + +
+ {{ cert.rendered_body|safe }} +
+ + +
+
+
مهر و امضای تایید کننده
+
{{ template.company.name }}
+ {% if template.company and template.company.signature %} + seal + {% endif %} +
-
- -{% endblock %} - + + + diff --git a/certificates/templates/certificates/step.html b/certificates/templates/certificates/step.html index b8923c2..0027d18 100644 --- a/certificates/templates/certificates/step.html +++ b/certificates/templates/certificates/step.html @@ -18,40 +18,49 @@ - {% endblock %} {% block content %} {% include '_toasts.html' %} + + + {% instance_info_modal instance %} + {% csrf_token %}
-
+

{{ step.name }}: {{ instance.process.name }}

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
-
+
{% stepper_header instance step %}
+
+
+
شماره درخواست: {{ instance.code }}
+
تاریخ: {{ cert.jissued_at }}
+
+
{% if template.company and template.company.logo %} logo @@ -62,21 +71,22 @@
{{ cert.rendered_body|safe }}
-
-
-
تاریخ صدور: {{ cert.issued_at }}
-
+
+
مهر و امضای تایید کننده
+
{{ template.company.name }}
{% if template.company and template.company.signature %} - seal + seal {% endif %} -
مهر و امضای شرکت