diff --git a/_base/settings.py b/_base/settings.py index e9079ca..8261409 100644 --- a/_base/settings.py +++ b/_base/settings.py @@ -173,6 +173,3 @@ 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 a7adccb..e4ee804 100644 --- a/_helpers/utils.py +++ b/_helpers/utils.py @@ -144,7 +144,7 @@ def persian_converter2(time): def persian_converter3(time): - time = time + time = time + datetime.timedelta(days=1) 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 7ffe250..d741357 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -16,8 +16,6 @@ class ProfileAdmin(admin.ModelAdmin): list_display = [ "user", "fullname", - "user_type_display", - "company_name", "pic_tag", "roles_str", "affairs", @@ -27,52 +25,8 @@ class ProfileAdmin(admin.ModelAdmin): "is_active", "jcreated", ] - 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',), - }), - ) + search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number'] + list_filter = ['user', 'roles', 'affairs', 'county', 'broker'] date_hierarchy = 'created' ordering = ['-created'] readonly_fields = ['created', 'updated'] diff --git a/accounts/forms.py b/accounts/forms.py index a5d493b..76beb31 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, USER_TYPE_CHOICES +from common.consts import UserRoles User = get_user_model() @@ -28,15 +28,10 @@ class CustomerForm(forms.ModelForm): class Meta: model = Profile fields = [ - 'user_type', 'phone_number_1', 'phone_number_2', 'national_code', - 'company_name', 'company_national_id', + 'phone_number_1', 'phone_number_2', 'national_code', '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' @@ -51,15 +46,6 @@ 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': 'آدرس کامل', @@ -81,12 +67,9 @@ 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': 'شماره حساب', @@ -106,21 +89,6 @@ 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: @@ -132,15 +100,7 @@ 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()) - - # 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]) + 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) @@ -165,12 +125,9 @@ 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'), @@ -214,12 +171,9 @@ 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 deleted file mode 100644 index 0d2edc1..0000000 --- a/accounts/migrations/0007_historicalprofile_company_name_and_more.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 348304e..937c329 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, USER_TYPE_CHOICES +from common.consts import UserRoles, BANK_CHOICES from locations.models import Affairs, Broker, County @@ -88,33 +88,6 @@ 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", @@ -206,23 +179,6 @@ 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 c1e2c0d..24f5e5a 100644 --- a/accounts/templates/accounts/customer_list.html +++ b/accounts/templates/accounts/customer_list.html @@ -61,7 +61,6 @@ ردیف کاربر - نوع کاربر کد ملی تلفن آدرس @@ -101,27 +100,6 @@ - - {% if customer.user_type == 'legal' %} - - حقوقی - -
- {% if customer.company_name %} - {{ customer.company_name|truncatechars:25 }} - {% endif %} - {% if customer.company_national_id %} - - {{ customer.company_national_id }} - - {% endif %} -
- {% else %} - - حقیقی - - {% endif %} - {{ customer.national_code|default:"کد ملی ثبت نشده" }}
@@ -227,16 +205,6 @@ -
- -
- - {{ form.user_type }} -
- {% if form.user_type.errors %} -
{{ form.user_type.errors.0 }}
- {% endif %} -
@@ -293,29 +261,6 @@ {% endif %}
- - - - -
@@ -402,18 +347,6 @@ کد ملی - - - نوع کاربر - - - - - نام شرکت - - - - - شناسه ملی شرکت - - - شماره تلفن اول - @@ -562,9 +495,6 @@ lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]], order: [[0, 'asc']], responsive: true, - columnDefs: [ - { targets: [8], orderable: false } // عملیات column غیرقابل مرتب‌سازی - ] }); // Handle form submission @@ -673,21 +603,6 @@ $('#cd-username').text(c.user.username || '-'); $('#cd-fullname').text(c.user.full_name || '-'); $('#cd-national-code').text(c.national_code || '-'); - - // User type and company information - const userTypeDisplay = c.user_type === 'legal' ? 'حقوقی' : 'حقیقی'; - $('#cd-user-type').text(userTypeDisplay); - - if (c.user_type === 'legal') { - $('#cd-company-name').text(c.company_name || '-'); - $('#cd-company-id').text(c.company_national_id || '-'); - $('#cd-company-name-row').show(); - $('#cd-company-id-row').show(); - } else { - $('#cd-company-name-row').hide(); - $('#cd-company-id-row').hide(); - } - $('#cd-phone1').text(c.phone_number_1 || '-'); $('#cd-phone2').text(c.phone_number_2 || '-'); $('#cd-email').text(c.user.email || '-'); @@ -774,12 +689,9 @@ 'customer-id': customer.id, 'id_first_name': customer.first_name, 'id_last_name': customer.last_name, - 'user-type-select': customer.user_type, 'id_phone_number_1': customer.phone_number_1, 'id_phone_number_2': customer.phone_number_2, 'id_national_code': customer.national_code, - 'id_company_name': customer.company_name, - 'id_company_national_id': customer.company_national_id, 'id_card_number': customer.card_number, 'id_account_number': customer.account_number, 'id_address': customer.address, @@ -799,14 +711,6 @@ if (customer.bank_name !== undefined && customer.bank_name !== null) { $('#id_bank_name').val(customer.bank_name); } - - // Ensure user type is applied and toggle company fields - if (customer.user_type !== undefined && customer.user_type !== null) { - $('#user-type-select').val(customer.user_type); - } - - // Toggle company fields based on user type - toggleCompanyFields(); // Open modal $('#add-new-record').offcanvas('show'); @@ -849,39 +753,8 @@ $('.is-invalid').removeClass('is-invalid'); $('.invalid-feedback').remove(); - // Reset user type to individual and hide company fields - $('#user-type-select').val('individual'); - toggleCompanyFields(); - // Open modal $('#add-new-record').offcanvas('show'); } - - function toggleCompanyFields() { - const userType = $('#user-type-select').val(); - const companyFields = $('.company-fields'); - - if (userType === 'legal') { - companyFields.show(); - // Make company fields required - $('input[name="company_name"]').attr('required', true); - $('input[name="company_national_id"]').attr('required', true); - } else { - companyFields.hide(); - // Remove required attribute from company fields - $('input[name="company_name"]').removeAttr('required').val(''); - $('input[name="company_national_id"]').removeAttr('required').val(''); - // Clear any validation errors for company fields - $('.company-fields .is-invalid').removeClass('is-invalid'); - $('.company-fields .invalid-feedback').remove(); - } - } - - // Initialize user type toggle functionality - $(document).ready(function() { - $('#user-type-select').on('change', toggleCompanyFields); - // Initialize on page load - toggleCompanyFields(); - }); {% endblock %} \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index ab85e27..fea6375 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -41,7 +41,7 @@ def dashboard(request): @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def customer_list(request): # Get all profiles that have customer role base = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user') @@ -56,7 +56,7 @@ def customer_list(request): @require_POST @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@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) @@ -96,7 +96,7 @@ def add_customer_ajax(request): @require_POST @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@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) @@ -148,12 +148,9 @@ def get_customer_data(request, customer_id): form_html = { 'first_name': str(form['first_name']), 'last_name': str(form['last_name']), - 'user_type': str(form['user_type']), 'phone_number_1': str(form['phone_number_1']), 'phone_number_2': str(form['phone_number_2']), 'national_code': str(form['national_code']), - 'company_name': str(form['company_name']), - 'company_national_id': str(form['company_national_id']), 'card_number': str(form['card_number']), 'account_number': str(form['account_number']), 'address': str(form['address']), @@ -166,12 +163,9 @@ def get_customer_data(request, customer_id): 'id': customer.id, 'first_name': customer.user.first_name, 'last_name': customer.user.last_name, - 'user_type': customer.user_type or 'individual', 'phone_number_1': customer.phone_number_1 or '', 'phone_number_2': customer.phone_number_2 or '', 'national_code': customer.national_code or '', - 'company_name': customer.company_name or '', - 'company_national_id': customer.company_national_id or '', 'card_number': customer.card_number or '', 'account_number': customer.account_number or '', 'address': customer.address or '', @@ -183,7 +177,7 @@ def get_customer_data(request, customer_id): @require_GET @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def get_customer_details(request, customer_id): """جزئیات کامل مشترک برای نمایش در مدال""" customer = get_object_or_404( @@ -202,9 +196,6 @@ def get_customer_details(request, customer_id): 'date_joined': customer.jcreated_date() if customer.user.date_joined else '', }, 'national_code': customer.national_code or '', - 'user_type': customer.user_type or 'individual', - 'company_name': customer.company_name or '', - 'company_national_id': customer.company_national_id or '', 'phone_number_1': customer.phone_number_1 or '', 'phone_number_2': customer.phone_number_2 or '', 'card_number': customer.card_number or '', @@ -238,7 +229,7 @@ def get_customer_details(request, customer_id): @require_GET @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def get_customer_wells(request, customer_id): """چاه‌های مرتبط با یک مشترک""" customer = get_object_or_404(Profile, id=customer_id) @@ -271,7 +262,7 @@ def get_customer_wells(request, customer_id): @require_GET @login_required -@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT, UserRoles.WATER_RESOURCE_MANAGER]) +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def get_customer_requests(request, customer_id): """درخواست‌های مرتبط با یک مشترک""" customer = get_object_or_404(Profile, id=customer_id) diff --git a/certificates/migrations/0002_certificateinstance_hologram_code.py b/certificates/migrations/0002_certificateinstance_hologram_code.py deleted file mode 100644 index 38f81b2..0000000 --- a/certificates/migrations/0002_certificateinstance_hologram_code.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-09-27 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('certificates', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='certificateinstance', - name='hologram_code', - field=models.CharField(blank=True, max_length=50, null=True, verbose_name='کد یکتا هولوگرام'), - ), - ] diff --git a/certificates/models.py b/certificates/models.py index a7afe72..1b3dcaf 100644 --- a/certificates/models.py +++ b/certificates/models.py @@ -28,7 +28,6 @@ class CertificateInstance(BaseModel): issued_at = models.DateField(auto_now_add=True, verbose_name='تاریخ صدور') approved = models.BooleanField(default=False, verbose_name='تایید شده') approved_at = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تایید') - hologram_code = models.CharField(max_length=50, null=True, blank=True, verbose_name='کد یکتا هولوگرام') class Meta: verbose_name = 'گواهی' diff --git a/certificates/templates/certificates/print.html b/certificates/templates/certificates/print.html index 39b028a..d5ef11f 100644 --- a/certificates/templates/certificates/print.html +++ b/certificates/templates/certificates/print.html @@ -18,20 +18,19 @@
-
+
-
کد یکتا هولوگرام: {{ cert.hologram_code|default:'-' }}
شماره درخواست: {{ instance.code }}
تاریخ: {{ cert.jissued_at }}
@@ -39,7 +38,10 @@
-

{{ cert.rendered_title }}

+ {% if template.company and template.company.logo %} + logo + {% endif %} +

{{ cert.rendered_title }}

{% if template.company %}
{{ template.company.name }}
{% endif %} @@ -49,41 +51,17 @@
{{ cert.rendered_body|safe }}
-
مشخصات چاه و کنتور هوشمند
-
-
-
موقعیت مکانی (UTM): {{ latest_report.utm_x|default:'-' }} , {{ latest_report.utm_y|default:'-' }}
-
نیرو محرکه چاه: {{ latest_report.driving_force|default:'-' }}
-
نوع کنتور: {{ latest_report.get_meter_type_display|default:'-' }}
-
قطر لوله آبده (اینچ): {{ latest_report.discharge_pipe_diameter|default:'-' }}
-
نوع مصرف: {{ latest_report.get_usage_type_display|default:'-' }}
-
شماره سیم‌کارت: {{ latest_report.sim_number|default:'-' }}
-
-
-
سایز کنتور: {{ latest_report.meter_size|default:'-' }}
-
شماره پروانه بهره‌برداری چاه: {{ latest_report.exploitation_license_number|default:'-' }}
-
قدرت موتور: {{ latest_report.motor_power|default:'-' }}
-
دبی قبل از کالیبراسیون: {{ latest_report.pre_calibration_flow_rate|default:'-' }}
-
دبی بعد از کالیبراسیون: {{ latest_report.post_calibration_flow_rate|default:'-' }}
-
نام شرکت کنتورساز: {{ latest_report.water_meter_manufacturer.name|default:'-' }}
-
شماره سریال کنتور: {{ instance.well.water_meter_serial_number|default:'-' }}
-
-
- -
-
-
مهر و امضای تایید کننده
-
{{ template.company.name }}
- {% if template.company and template.company.signature %} - seal - {% endif %} -
-
-
+ +
+
+
مهر و امضای تایید کننده
+
{{ template.company.name }}
+ {% if template.company and template.company.signature %} + seal + {% endif %} +
- -
{% endblock %} diff --git a/processes/templatetags/processes_tags.py b/processes/templatetags/processes_tags.py index 5087f4e..5ee7b2f 100644 --- a/processes/templatetags/processes_tags.py +++ b/processes/templatetags/processes_tags.py @@ -28,11 +28,9 @@ def stepper_header(instance, current_step=None): status = step_id_to_status.get(step.id, 'pending') # بررسی دسترسی به مرحله (UI navigation constraints): - can_access = instance.can_access_step(step) - + # can_access = instance.can_access_step(step) # فقط مراحل تکمیل‌شده یا مرحله جاری قابل کلیک هستند - # can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id) - + can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id) # مرحله انتخاب‌شده (نمایش فعلی) is_selected = bool(current_step and step.id == current_step.id) # مرحله‌ای که باید انجام شود (مرحله جاری در instance) diff --git a/processes/urls.py b/processes/urls.py index 0e1aaa5..a58ebab 100644 --- a/processes/urls.py +++ b/processes/urls.py @@ -6,7 +6,6 @@ app_name = 'processes' urlpatterns = [ # Requests UI path('requests/', views.request_list, name='request_list'), - path('requests/export/excel/', views.export_requests_excel, name='export_requests_excel'), path('requests/create/', views.create_request_with_entities, name='create_request_with_entities'), path('requests/lookup/well/', views.lookup_well_by_subscription, name='lookup_well_by_subscription'), path('requests/lookup/representative/', views.lookup_representative_by_national_code, name='lookup_representative_by_national_code'), diff --git a/processes/utils.py b/processes/utils.py index 717c0fc..951398e 100644 --- a/processes/utils.py +++ b/processes/utils.py @@ -20,7 +20,7 @@ def scope_instances_queryset(user, queryset=None): return qs.filter(id__in=assign_ids) if profile.has_role(UserRoles.BROKER): return qs.filter(broker=profile.broker) - if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER): + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): return qs.filter(broker__affairs__county=profile.county) if profile.has_role(UserRoles.ADMIN): return qs @@ -69,7 +69,7 @@ def scope_wells_queryset(user, queryset=None): return qs if profile.has_role(UserRoles.BROKER): return qs.filter(broker=profile.broker) - if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER): + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): return qs.filter(broker__affairs__county=profile.county) if profile.has_role(UserRoles.INSTALLER): # Wells that have instances assigned to this installer @@ -102,7 +102,7 @@ def scope_customers_queryset(user, queryset=None): return qs if profile.has_role(UserRoles.BROKER): return qs.filter(broker=profile.broker) - if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER) or profile.has_role(UserRoles.WATER_RESOURCE_MANAGER): + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): return qs.filter(county=profile.county) if profile.has_role(UserRoles.INSTALLER): # Customers that are representatives of instances assigned to this installer diff --git a/processes/views.py b/processes/views.py index d96a86b..38ad946 100644 --- a/processes/views.py +++ b/processes/views.py @@ -3,19 +3,13 @@ from django.urls import reverse from django.contrib.auth.decorators import login_required from django.contrib import messages -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse from django.views.decorators.http import require_POST, require_GET -from django.utils import timezone from django.db import transaction from django.contrib.auth import get_user_model -import openpyxl -from openpyxl.styles import Font, Alignment, PatternFill -from openpyxl.utils import get_column_letter -from datetime import datetime -from _helpers.utils import persian_converter3 from .models import Process, ProcessInstance, StepInstance, ProcessStep from .utils import scope_instances_queryset, get_scoped_instance_or_404 -from installations.models import InstallationAssignment, InstallationReport +from installations.models import InstallationAssignment from wells.models import Well from accounts.models import Profile, Broker from locations.models import Affairs @@ -71,65 +65,18 @@ def request_list(request): steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order') manufacturers = WaterMeterManufacturer.objects.all().order_by('name') - # Prepare installation assignments map (scheduled date by instance) - try: - instance_ids = list(instances.values_list('id', flat=True)) - except Exception: - instance_ids = [] - assignments_map = {} - reports_map = {} - if instance_ids: - try: - ass_qs = InstallationAssignment.objects.filter(process_instance_id__in=instance_ids).values('process_instance_id', 'scheduled_date') - for row in ass_qs: - assignments_map[row['process_instance_id']] = row['scheduled_date'] - except Exception: - assignments_map = {} - # latest report per instance (visited_date) - try: - rep_qs = InstallationReport.objects.filter(assignment__process_instance_id__in=instance_ids).order_by('-created').values('assignment__process_instance_id', 'visited_date') - for row in rep_qs: - pid = row['assignment__process_instance_id'] - if pid not in reports_map: - reports_map[pid] = row['visited_date'] - except Exception: - reports_map = {} - - # Calculate progress for each instance and attach install schedule info + # Calculate progress for each instance instances_with_progress = [] for instance in instances: total_steps = instance.process.steps.count() completed_steps = instance.step_instances.filter(status='completed').count() progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0 - sched_date = assignments_map.get(instance.id) - overdue_days = 0 - reference_date = None - if sched_date: - # Reference date: until installer submits a report, use today; otherwise use visited_date - try: - visited_date = reports_map.get(instance.id) - if visited_date: - reference_date = visited_date - else: - try: - reference_date = timezone.localdate() - except Exception: - from datetime import date as _date - reference_date = _date.today() - if reference_date > sched_date: - overdue_days = (reference_date - sched_date).days - except Exception: - overdue_days = 0 - reference_date = None - - installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date + instances_with_progress.append({ 'instance': instance, 'progress_percentage': round(progress_percentage), 'completed_steps': completed_steps, 'total_steps': total_steps, - 'installation_scheduled_date': installation_scheduled_date, - 'installation_overdue_days': overdue_days, }) # Summary stats for header cards @@ -213,10 +160,7 @@ def lookup_representative_by_national_code(request): 'last_name': user.last_name, 'full_name': user.get_full_name(), 'profile': { - 'user_type': profile.user_type, 'national_code': profile.national_code, - 'company_name': profile.company_name, - 'company_national_id': profile.company_national_id, 'phone_number_1': profile.phone_number_1, 'phone_number_2': profile.phone_number_2, 'card_number': profile.card_number, @@ -296,7 +240,6 @@ def create_request_with_entities(request): well = existing well_data = request.POST.copy() - print(well_data) # Ensure representative set from created/selected user if not provided if representative_user and not well_data.get('representative'): well_data['representative'] = str(representative_user.id) @@ -423,12 +366,12 @@ def step_detail(request, instance_id, step_id): return redirect('processes:instance_summary', instance_id=instance.id) # جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیل‌شده - # try: - # if instance.current_step and step.order > instance.current_step.order: - # messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.') - # return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id) - # except Exception: - # pass + try: + if instance.current_step and step.order > instance.current_step.order: + messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.') + return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id) + except Exception: + pass # بررسی دسترسی به مرحله if not instance.can_access_step(step): @@ -528,365 +471,4 @@ def instance_summary(request, instance_id): 'latest_report': latest_report, 'certificate': certificate, }) - - -def format_date_jalali(date_obj): - """Convert date to Jalali format without time""" - if not date_obj: - return "" - try: - # If it's a datetime, get just the date part - if hasattr(date_obj, 'date'): - date_obj = date_obj.date() - return persian_converter3(date_obj) - except Exception: - return "" - -def format_datetime_jalali(datetime_obj): - """Convert datetime to Jalali format without time""" - if not datetime_obj: - return "" - try: - # Get just the date part - date_part = datetime_obj.date() if hasattr(datetime_obj, 'date') else datetime_obj - return persian_converter3(date_part) - except Exception: - return "" - -@login_required -def export_requests_excel(request): - """Export filtered requests to Excel""" - - # Get the same queryset as request_list view (with filters) - instances = ProcessInstance.objects.select_related( - 'process', 'current_step', 'representative', 'well', 'well__county', 'well__affairs' - ).prefetch_related('step_instances') - - # Apply scoping - instances = scope_instances_queryset(request.user, instances) - - # Apply filters (same logic as request_list view) - filter_status = request.GET.get('status', '').strip() - if filter_status: - instances = instances.filter(status=filter_status) - - filter_affairs = request.GET.get('affairs', '').strip() - if filter_affairs and filter_affairs.isdigit(): - instances = instances.filter(well__affairs_id=filter_affairs) - - filter_broker = request.GET.get('broker', '').strip() - if filter_broker and filter_broker.isdigit(): - instances = instances.filter(well__broker_id=filter_broker) - - filter_step = request.GET.get('step', '').strip() - if filter_step and filter_step.isdigit(): - instances = instances.filter(current_step_id=filter_step) - - # Get installation data - assignment_ids = list(instances.values_list('id', flat=True)) - assignments_map = {} - reports_map = {} - installers_map = {} - - if assignment_ids: - assignments = InstallationAssignment.objects.filter( - process_instance_id__in=assignment_ids - ).select_related('process_instance', 'installer') - assignments_map = {a.process_instance_id: a.scheduled_date for a in assignments} - installers_map = {a.process_instance_id: a.installer for a in assignments} - - reports = InstallationReport.objects.filter( - assignment__process_instance_id__in=assignment_ids - ).select_related('assignment') - reports_map = {r.assignment.process_instance_id: r for r in reports} - - # Get quotes and payments data - from invoices.models import Quote, Payment, Invoice - quotes_map = {} - payments_map = {} - settlement_dates_map = {} - approval_dates_map = {} - approval_users_map = {} - - if assignment_ids: - # Get quotes - quotes = Quote.objects.filter( - process_instance_id__in=assignment_ids - ).select_related('process_instance') - quotes_map = {q.process_instance_id: q for q in quotes} - - # Get payments with reference numbers - payments = Payment.objects.filter( - invoice__process_instance_id__in=assignment_ids, - is_deleted=False - ).select_related('invoice__process_instance').order_by('created') - - for payment in payments: - if payment.invoice.process_instance_id not in payments_map: - payments_map[payment.invoice.process_instance_id] = [] - payments_map[payment.invoice.process_instance_id].append(payment) - - # Get final invoices to check settlement dates - invoices = Invoice.objects.filter( - process_instance_id__in=assignment_ids - ).select_related('process_instance') - - for invoice in invoices: - if invoice.remaining_amount == 0: # Fully settled - # Find the last payment date for this invoice - last_payment = Payment.objects.filter( - invoice__process_instance=invoice.process_instance, - is_deleted=False - ).order_by('-created').first() - if last_payment: - settlement_dates_map[invoice.process_instance_id] = last_payment.created - - # Get installation approval data - from processes.models import StepInstance, StepApproval - installation_steps = StepInstance.objects.filter( - process_instance_id__in=assignment_ids, - step__slug='installation_report', # Assuming this is the slug for installation step - status='completed' - ).select_related('process_instance') - - for step_instance in installation_steps: - # Get the approval that completed this step - approval = StepApproval.objects.filter( - step_instance=step_instance, - decision='approved', - is_deleted=False - ).select_related('approved_by').order_by('-created').first() - - if approval: - approval_dates_map[step_instance.process_instance_id] = approval.created - approval_users_map[step_instance.process_instance_id] = approval.approved_by - - # Calculate progress and installation data - instances_with_progress = [] - for instance in instances: - total_steps = instance.process.steps.count() - completed_steps = instance.step_instances.filter(status='completed').count() - progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0 - - sched_date = assignments_map.get(instance.id) - overdue_days = 0 - reference_date = None - - if sched_date: - try: - report = reports_map.get(instance.id) - if report and report.visited_date: - reference_date = report.visited_date - else: - try: - reference_date = timezone.localdate() - except Exception: - from datetime import date as _date - reference_date = _date.today() - if reference_date > sched_date: - overdue_days = (reference_date - sched_date).days - except Exception: - overdue_days = 0 - - installation_scheduled_date = reference_date if reference_date and reference_date > sched_date else sched_date - - instances_with_progress.append({ - 'instance': instance, - 'progress_percentage': round(progress_percentage), - 'completed_steps': completed_steps, - 'total_steps': total_steps, - 'installation_scheduled_date': installation_scheduled_date, - 'installation_overdue_days': overdue_days, - }) - - # Create Excel workbook - wb = openpyxl.Workbook() - ws = wb.active - ws.title = "لیست درخواست‌ها" - - # Set RTL (Right-to-Left) direction - ws.sheet_view.rightToLeft = True - - # Define column headers - headers = [ - 'شناسه', - 'تاریخ ایجاد درخواست', - 'نام نماینده', - 'نام خانوادگی نماینده', - 'کد ملی نماینده', - 'نام شرکت', - 'شناسه شرکت', - 'سریال کنتور', - 'سریال کنتور جدید', - 'شماره اشتراک آب', - 'شماره اشتراک برق', - 'قدرت چاه', - 'شماره تماس ۱', - 'شماره تماس ۲', - 'آدرس', - 'مبلغ پیش‌فاکتور', - 'تاریخ واریزی‌ها و کدهای رهگیری', - 'تاریخ مراجعه نصاب', - 'تاخیر نصاب', - 'نام نصاب', - 'تاریخ تایید نصب توسط مدیر', - 'نام تایید کننده نصب', - 'تاریخ تسویه' - ] - - # Write headers - for col, header in enumerate(headers, 1): - cell = ws.cell(row=1, column=col, value=header) - cell.font = Font(bold=True) - cell.alignment = Alignment(horizontal='center') - cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") - - # Write data rows - for row_num, item in enumerate(instances_with_progress, 2): - instance = item['instance'] - - # Get representative info - rep_first_name = "" - rep_last_name = "" - rep_national_code = "" - rep_phone_1 = "" - rep_phone_2 = "" - rep_address = "" - company_name = "" - company_national_id = "" - - if instance.representative: - rep_first_name = instance.representative.first_name or "" - rep_last_name = instance.representative.last_name or "" - if hasattr(instance.representative, 'profile') and instance.representative.profile: - profile = instance.representative.profile - rep_national_code = profile.national_code or "" - rep_phone_1 = profile.phone_number_1 or "" - rep_phone_2 = profile.phone_number_2 or "" - rep_address = profile.address or "" - if profile.user_type == 'legal': - company_name = profile.company_name or "" - company_national_id = profile.company_national_id or "" - - # Get well info - water_subscription = "" - electricity_subscription = "" - well_power = "" - old_meter_serial = "" - if instance.well: - water_subscription = instance.well.water_subscription_number or "" - electricity_subscription = instance.well.electricity_subscription_number or "" - well_power = str(instance.well.well_power) if instance.well.well_power else "" - old_meter_serial = instance.well.water_meter_serial_number or "" - - # Get new meter serial from installation report - new_meter_serial = "" - installer_visit_date = "" - report = reports_map.get(instance.id) - if report: - new_meter_serial = report.new_water_meter_serial or "" - installer_visit_date = format_date_jalali(report.visited_date) - - # Get quote amount - quote_amount = "" - quote = quotes_map.get(instance.id) - if quote: - quote_amount = str(quote.final_amount) if quote.final_amount else "" - - # Get payments info - payments_info = "" - payments = payments_map.get(instance.id, []) - if payments: - payment_strings = [] - for payment in payments: - date_str = format_datetime_jalali(payment.created) - reference_number = payment.reference_number or "بدون کد" - payment_strings.append(f"{date_str} - {reference_number}") - payments_info = " | ".join(payment_strings) - - # Get installer name - installer_name = "" - installer = installers_map.get(instance.id) - if installer: - installer_name = installer.get_full_name() or str(installer) - - # Get overdue days - overdue_days = "" - if item['installation_overdue_days'] and item['installation_overdue_days'] > 0: - overdue_days = str(item['installation_overdue_days']) - - # Get approval info - approval_date = "" - approval_user = "" - approval_date_obj = approval_dates_map.get(instance.id) - approval_user_obj = approval_users_map.get(instance.id) - if approval_date_obj: - approval_date = format_datetime_jalali(approval_date_obj) - if approval_user_obj: - approval_user = approval_user_obj.get_full_name() or str(approval_user_obj) - - # Get settlement date - settlement_date = "" - settlement_date_obj = settlement_dates_map.get(instance.id) - if settlement_date_obj: - settlement_date = format_datetime_jalali(settlement_date_obj) - - row_data = [ - instance.code, # شناسه - format_datetime_jalali(instance.created), # تاریخ ایجاد درخواست - rep_first_name, # نام نماینده - rep_last_name, # نام خانوادگی نماینده - rep_national_code, # کد ملی نماینده - company_name, # نام شرکت - company_national_id, # شناسه شرکت - old_meter_serial, # سریال کنتور - new_meter_serial, # سریال کنتور جدید - water_subscription, # شماره اشتراک آب - electricity_subscription, # شماره اشتراک برق - well_power, # قدرت چاه - rep_phone_1, # شماره تماس ۱ - rep_phone_2, # شماره تماس ۲ - rep_address, # آدرس - quote_amount, # مبلغ پیش‌فاکتور - payments_info, # تاریخ واریزی‌ها و کدهای رهگیری - installer_visit_date, # تاریخ مراجعه نصاب - overdue_days, # تاخیر نصاب - installer_name, # نام نصاب - approval_date, # تاریخ تایید نصب توسط مدیر - approval_user, # نام تایید کننده نصب - settlement_date # تاریخ تسویه - ] - - for col, value in enumerate(row_data, 1): - cell = ws.cell(row=row_num, column=col, value=value) - # Set right alignment for Persian text - cell.alignment = Alignment(horizontal='right') - - # Auto-adjust column widths - for col in range(1, len(headers) + 1): - column_letter = get_column_letter(col) - max_length = 0 - for row in ws[column_letter]: - try: - if len(str(row.value)) > max_length: - max_length = len(str(row.value)) - except: - pass - adjusted_width = min(max_length + 2, 50) - ws.column_dimensions[column_letter].width = adjusted_width - - # Prepare response - response = HttpResponse( - content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ) - - # Generate filename with current date - current_date = datetime.now().strftime('%Y%m%d_%H%M') - filename = f'requests_export_{current_date}.xlsx' - response['Content-Disposition'] = f'attachment; filename="{filename}"' - - # Save workbook to response - wb.save(response) - - return response \ No newline at end of file diff --git a/templates/sidebars/admin.html b/templates/sidebars/admin.html index 3e0d49a..066c177 100644 --- a/templates/sidebars/admin.html +++ b/templates/sidebars/admin.html @@ -114,7 +114,7 @@ - {% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant or request.user|is_water_resource_manager %} + {% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant %}