diff --git a/_base/settings.py b/_base/settings.py index 8261409..e53a6f1 100644 --- a/_base/settings.py +++ b/_base/settings.py @@ -167,7 +167,7 @@ JAZZMIN_SETTINGS = { # Copyright on the footer "copyright": "سامانه شفافیت", # Logo to use for your site, must be present in static files, used for brand on top left - # "site_logo": "../static/dist/img/iconlogo.png", + "site_logo": "../static/dist/img/iconlogo.png", # Relative paths to custom CSS/JS scripts (must be present in static files) "custom_css": "../static/admin/css/custom_rtl.css", "custom_js": None, diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 7b35fb7..de431d4 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-14 09:02 import django.core.validators import django.db.models.deletion @@ -17,27 +17,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Company', - 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='نام')), - ('logo', models.ImageField(blank=True, null=True, upload_to='companies/logos', verbose_name='لوگوی شرکت')), - ('signature', models.ImageField(blank=True, null=True, upload_to='companies/signatures', verbose_name='امضای شرکت')), - ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), - ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس')), - ], - options={ - 'verbose_name': 'شرکت', - 'verbose_name_plural': 'شرکت\u200cها', - }, - ), migrations.CreateModel( name='HistoricalProfile', fields=[ @@ -51,7 +30,6 @@ class Migration(migrations.Migration): ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')), ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')), - ('bank_name', 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='نام بانک')), ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')), ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')), ('pic', models.TextField(default='../static/sample_images/profile.jpg', max_length=100, verbose_name='تصویر')), @@ -106,7 +84,6 @@ class Migration(migrations.Migration): ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), ('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')), ('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')), - ('bank_name', 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='نام بانک')), ('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')), ('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')), ('pic', models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر')), diff --git a/accounts/migrations/0002_company.py b/accounts/migrations/0002_company.py new file mode 100644 index 0000000..c944cdf --- /dev/null +++ b/accounts/migrations/0002_company.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.4 on 2025-08-21 06:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Company', + 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='نام')), + ('logo', models.ImageField(blank=True, null=True, upload_to='companies/logos', verbose_name='لوگوی شرکت')), + ('signature', models.ImageField(blank=True, null=True, upload_to='companies/signatures', verbose_name='امضای شرکت')), + ('address', models.TextField(blank=True, null=True, verbose_name='آدرس')), + ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس')), + ], + options={ + 'verbose_name': 'شرکت', + 'verbose_name_plural': 'شرکت\u200cها', + }, + ), + ] diff --git a/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py b/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py new file mode 100644 index 0000000..6becfec --- /dev/null +++ b/accounts/migrations/0003_historicalprofile_bank_name_profile_bank_name.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-08-21 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_company'), + ] + + operations = [ + migrations.AddField( + model_name='historicalprofile', + 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='profile', + 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='نام بانک'), + ), + ] diff --git a/certificates/migrations/0001_initial.py b/certificates/migrations/0001_initial.py index b5753ec..83533bd 100644 --- a/certificates/migrations/0001_initial.py +++ b/certificates/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-22 09:58 import django.db.models.deletion from django.db import migrations, models @@ -9,7 +9,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('accounts', '0001_initial'), ('processes', '0001_initial'), ] @@ -24,8 +23,10 @@ class Migration(migrations.Migration): ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')), ('title', models.CharField(max_length=200, verbose_name='عنوان')), ('body', models.TextField(verbose_name='متن قالب (با جایگزین\u200cها)')), + ('company_logo', models.ImageField(blank=True, null=True, upload_to='certificates/logos/%Y/%m/%d/', verbose_name='لوگو')), + ('company_name', models.CharField(blank=True, max_length=200, verbose_name='نام شرکت')), + ('company_seal_signature', models.ImageField(blank=True, null=True, upload_to='certificates/seals/%Y/%m/%d/', verbose_name='مهر و امضا')), ('is_active', models.BooleanField(default=True, verbose_name='فعال')), - ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت صادر کننده')), ], options={ 'verbose_name': 'قالب گواهی', diff --git a/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py b/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py new file mode 100644 index 0000000..e929c5a --- /dev/null +++ b/certificates/migrations/0002_remove_certificatetemplate_company_logo_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.4 on 2025-08-22 10:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_historicalprofile_bank_name_profile_bank_name'), + ('certificates', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='certificatetemplate', + name='company_logo', + ), + migrations.RemoveField( + model_name='certificatetemplate', + name='company_name', + ), + migrations.RemoveField( + model_name='certificatetemplate', + name='company_seal_signature', + ), + migrations.AddField( + model_name='certificatetemplate', + name='company', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت صادر کننده'), + ), + ] diff --git a/certificates/templates/certificates/step.html b/certificates/templates/certificates/step.html index b8923c2..3392f82 100644 --- a/certificates/templates/certificates/step.html +++ b/certificates/templates/certificates/step.html @@ -2,7 +2,6 @@ {% load static %} {% load processes_tags %} {% load humanize %} - {% load accounts_tags %} {% block sidebar %} {% include 'sidebars/admin.html' %} @@ -80,11 +79,7 @@ {% else %}{% endif %}
{% csrf_token %} - {% if request.user|is_broker %} - - {% else %} - - {% endif %} +
diff --git a/certificates/views.py b/certificates/views.py index 761ee83..428c5ba 100644 --- a/certificates/views.py +++ b/certificates/views.py @@ -9,7 +9,6 @@ from processes.models import ProcessInstance, StepInstance from invoices.models import Invoice from installations.models import InstallationReport from .models import CertificateTemplate, CertificateInstance -from common.consts import UserRoles from _helpers.jalali import Gregorian @@ -79,14 +78,6 @@ def certificate_step(request, instance_id, step_id): next_step = instance.process.steps.filter(order__gt=instance.current_step.order).first() if instance.current_step else None if request.method == 'POST': - # Only broker can approve and finish certificate step - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)): - messages.error(request, 'شما مجوز تایید این مرحله را ندارید') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) - except Exception: - messages.error(request, 'شما مجوز تایید این مرحله را ندارید') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) cert.approved = True cert.approved_at = timezone.now() cert.save() @@ -98,10 +89,7 @@ def certificate_step(request, instance_id, step_id): instance.current_step = next_step instance.save() return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) - # Mark the whole process instance as completed on the last step - instance.status = 'completed' - instance.save() - return redirect('processes:instance_summary', instance_id=instance.id) + return redirect('processes:request_list') return render(request, 'certificates/step.html', { 'instance': instance, diff --git a/contracts/migrations/0001_initial.py b/contracts/migrations/0001_initial.py index 25acea6..8f9f4b7 100644 --- a/contracts/migrations/0001_initial.py +++ b/contracts/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-21 06:00 import django.db.models.deletion +import simple_history.models from django.conf import settings from django.db import migrations, models @@ -10,7 +11,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('accounts', '0001_initial'), ('processes', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -28,7 +28,8 @@ class Migration(migrations.Migration): ('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')), ('name', models.CharField(max_length=100, verbose_name='نام')), ('body', models.TextField(verbose_name='متن قرارداد')), - ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت')), + ('company_logo', models.ImageField(blank=True, null=True, upload_to='contracts/logos/%Y/%m/%d/', verbose_name='لوگوی شرکت')), + ('company_signature', models.ImageField(blank=True, null=True, upload_to='contracts/signatures/%Y/%m/%d/', verbose_name='امضای شرکت')), ], options={ 'verbose_name': 'قالب قرارداد', @@ -57,4 +58,61 @@ class Migration(migrations.Migration): 'ordering': ['-created'], }, ), + migrations.CreateModel( + name='HistoricalContractInstance', + 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='تاریخ حذف')), + ('rendered_body', models.TextField(verbose_name='متن نهایی قرارداد')), + ('approved', models.BooleanField(default=False, verbose_name='تایید شده')), + ('approved_at', models.DateTimeField(blank=True, null=True, 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='ایجاد کننده')), + ('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='نمونه فرآیند')), + ('template', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='contracts.contracttemplate', verbose_name='قالب مورد استفاده')), + ], + 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='HistoricalContractTemplate', + 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='نام')), + ('body', models.TextField(verbose_name='متن قرارداد')), + ('company_logo', models.TextField(blank=True, max_length=100, null=True, verbose_name='لوگوی شرکت')), + ('company_signature', models.TextField(blank=True, max_length=100, null=True, 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)), + ('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), + ), ] diff --git a/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py b/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py new file mode 100644 index 0000000..60d434d --- /dev/null +++ b/contracts/migrations/0002_remove_historicalcontracttemplate_history_user_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.4 on 2025-08-21 06:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_company'), + ('contracts', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='historicalcontracttemplate', + name='history_user', + ), + migrations.RemoveField( + model_name='contracttemplate', + name='company_logo', + ), + migrations.RemoveField( + model_name='contracttemplate', + name='company_signature', + ), + migrations.AddField( + model_name='contracttemplate', + name='company', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.company', verbose_name='شرکت'), + ), + migrations.DeleteModel( + name='HistoricalContractInstance', + ), + migrations.DeleteModel( + name='HistoricalContractTemplate', + ), + ] diff --git a/contracts/templates/contracts/contract_step.html b/contracts/templates/contracts/contract_step.html index df4fdfc..33ff326 100644 --- a/contracts/templates/contracts/contract_step.html +++ b/contracts/templates/contracts/contract_step.html @@ -41,36 +41,32 @@
- {% if can_view_contract_body %} - {% if template.company.logo %} -
- لوگوی شرکت -

{{ contract.template.company.name }}

-
{{ contract.template.name }}
-
- {% endif %} + {% if template.company.logo %} +
+ لوگوی شرکت +

{{ contract.template.company.name }}

+
{{ contract.template.name }}
+
+ {% endif %} -
تاریخ: {{ contract.jcreated }}
-
-
{{ contract.rendered_body|safe }}
-
-
-
-
امضای مشترک
-
-
-
-
امضای شرکت
-
- {% if template.company.signature %} - امضای شرکت - {% endif %} -
+
تاریخ: {{ contract.jcreated }}
+
+
{{ contract.rendered_body|safe }}
+
+
+
+
امضای مشترک
+
+
+
+
امضای شرکت
+
+ {% if template.company.signature %} + امضای شرکت + {% endif %}
- {% else %} -
شما دسترسی به مشاهده متن قرارداد را ندارید.
- {% endif %} +
@@ -81,17 +77,9 @@ {% endif %} {% if next_step %} - {% if is_broker %} - - {% else %} - بعدی - {% endif %} + {% else %} - {% if is_broker %} - - {% else %} - - {% endif %} + {% endif %}
diff --git a/contracts/views.py b/contracts/views.py index 1949665..f2d0deb 100644 --- a/contracts/views.py +++ b/contracts/views.py @@ -4,7 +4,6 @@ from django.urls import reverse from django.utils import timezone from django.template import Template, Context from processes.models import ProcessInstance, StepInstance -from common.consts import UserRoles from .models import ContractTemplate, ContractInstance from _helpers.utils import jalali_converter2 @@ -35,20 +34,6 @@ def contract_step(request, instance_id, step_id): step = get_object_or_404(instance.process.steps, id=step_id) previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() - # Access control: - # - INSTALLER: can open step but cannot view contract body (show inline message) - # - Others: can view - # - Only BROKER can submit/complete this step - profile = getattr(request.user, 'profile', None) - is_broker = False - can_view_contract_body = True - try: - is_broker = bool(profile and profile.has_role(UserRoles.BROKER)) - if profile and profile.has_role(UserRoles.INSTALLER): - can_view_contract_body = False - except Exception: - pass - template_obj = ContractTemplate.objects.first() if not template_obj: return render(request, 'contracts/contract_missing.html', {'instance': instance}) @@ -69,11 +54,8 @@ def contract_step(request, instance_id, step_id): contract.rendered_body = rendered contract.save() - # If user submits to go next, only broker can complete and go to next + # If user submits to go next, mark this step completed and go to next if request.method == 'POST': - if not is_broker: - from django.http import JsonResponse - return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) StepInstance.objects.update_or_create( process_instance=instance, step=step, @@ -92,8 +74,6 @@ def contract_step(request, instance_id, step_id): 'template': template_obj, 'previous_step': previous_step, 'next_step': next_step, - 'is_broker': is_broker, - 'can_view_contract_body': can_view_contract_body, }) diff --git a/db.sqlite3 b/db.sqlite3 index d816e90..805325d 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/installations/migrations/0001_initial.py b/installations/migrations/0001_initial.py index 41f02f5..05cedcd 100644 --- a/installations/migrations/0001_initial.py +++ b/installations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-21 08:25 import django.db.models.deletion from django.conf import settings @@ -10,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('invoices', '0001_initial'), + ('invoices', '0002_historicalpayment_receipt_image_and_more'), ('processes', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -53,8 +53,6 @@ class Migration(migrations.Migration): ('utm_x', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='UTM X')), ('utm_y', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True, verbose_name='UTM Y')), ('description', models.TextField(blank=True, verbose_name='توضیحات')), - ('approved', models.BooleanField(default=False, verbose_name='تایید شده')), - ('approved_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تایید')), ('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports', to='installations.installationassignment', verbose_name='اختصاص')), ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='ایجادکننده')), ], diff --git a/installations/migrations/0002_installationreport_approved_and_more.py b/installations/migrations/0002_installationreport_approved_and_more.py new file mode 100644 index 0000000..d5df3c8 --- /dev/null +++ b/installations/migrations/0002_installationreport_approved_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-08-21 09:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('installations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='installationreport', + name='approved', + field=models.BooleanField(default=False, verbose_name='تایید شده'), + ), + migrations.AddField( + model_name='installationreport', + name='approved_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تایید'), + ), + ] diff --git a/installations/templates/installations/installation_assign_step.html b/installations/templates/installations/installation_assign_step.html index 2bdbe4d..c1d6c79 100644 --- a/installations/templates/installations/installation_assign_step.html +++ b/installations/templates/installations/installation_assign_step.html @@ -1,7 +1,6 @@ {% extends '_base.html' %} {% load static %} {% load processes_tags %} -{% load common_tags %} {% load humanize %} {% block sidebar %} @@ -42,15 +41,12 @@
- {% if show_denied_msg %} -
شما اجازه تعیین نصاب را ندارید.
- {% endif %}
{% csrf_token %}
- {% for p in installers %} @@ -59,39 +55,17 @@
- +
- {% if assignment.assigned_by or assignment.installer %} -
-
- {% if assignment.assigned_by %} -
-
تعیین‌کننده نصاب
-
{{ assignment.assigned_by.get_full_name|default:assignment.assigned_by.username }} ({{ assignment.assigned_by.username }})
-
- {% endif %} - {% if assignment.updated %} -
-
تاریخ ثبت/ویرایش
-
{{ assignment.updated|to_jalali }}
-
- {% endif %} -
-
- {% endif %}
{% if previous_step %} قبلی {% else %} {% endif %} - {% if is_manager %} - - {% else %} - بعدی - {% endif %} +
diff --git a/installations/templates/installations/installation_report_step.html b/installations/templates/installations/installation_report_step.html index 16b1eb6..275f7bc 100644 --- a/installations/templates/installations/installation_report_step.html +++ b/installations/templates/installations/installation_report_step.html @@ -2,7 +2,6 @@ {% load static %} {% load processes_tags %} {% load common_tags %} -{% load accounts_tags %} {% load humanize %} {% block sidebar %} @@ -42,31 +41,13 @@ {% stepper_header instance step %}
+ {% if report and not edit_mode %}
-
-
- {% if request.user|is_installer %} - ویرایش گزارش نصب - {% else %} - - {% endif %} - {% if user_can_approve %} - - - {% endif %} -
+
- {% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %} - - {% endif %}

تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}

@@ -86,9 +67,6 @@
{% endif %}
- {% if request.user|is_manager or request.user|is_admin %} -
- {% endif %}
عکس‌ها
{% for p in report.photos.all %} @@ -137,42 +115,6 @@
- {% if approver_statuses %} -
-
-
وضعیت تاییدها
- {% if user_can_approve %} -
- - -
- {% endif %} -
-
-
- {% for st in approver_statuses %} -
-
-
- {{ st.role.name }} - {% if st.status == 'approved' %} - تایید شد - {% elif st.status == 'rejected' %} - رد شد - {% else %} - در انتظار - {% endif %} -
- {% if st.status == 'rejected' and st.reason %} -
علت: {{ st.reason }}
- {% endif %} -
-
- {% endfor %} -
-
-
- {% endif %}
{% if previous_step %} @@ -185,9 +127,6 @@ {% endif %}
{% else %} - {% if not request.user|is_installer %} -
شما مجوز ثبت/ویرایش گزارش نصب را ندارید. اطلاعات به صورت فقط خواندنی نمایش داده می‌شود.
- {% endif %}
{% csrf_token %}
@@ -195,42 +134,40 @@
- +
- +
- +
- +
- +
- +
- +
- {% if request.user|is_installer %} - - {% endif %} +
{% if report %}
@@ -238,9 +175,7 @@
photo - {% if request.user|is_installer %} - - {% endif %} +
@@ -350,11 +285,7 @@ {% endif %}
- {% if request.user|is_installer %} - - {% else %} - - {% endif %} + {% if next_step %} بعدی {% endif %} @@ -367,58 +298,6 @@
- - - - - - - - - {% endblock %} {% block script %} @@ -566,3 +445,4 @@ {% endblock %} + diff --git a/installations/views.py b/installations/views.py index 8c3dc7e..4692886 100644 --- a/installations/views.py +++ b/installations/views.py @@ -5,8 +5,7 @@ from django.urls import reverse from django.utils import timezone from accounts.models import Profile from common.consts import UserRoles -from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval -from accounts.models import Role +from processes.models import ProcessInstance, StepInstance from invoices.models import Item, Quote, QuoteItem from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange from decimal import Decimal, InvalidOperation @@ -22,18 +21,7 @@ def installation_assign_step(request, instance_id, step_id): installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value).select_related('user').all() assignment, _ = InstallationAssignment.objects.get_or_create(process_instance=instance) - # Role flags - profile = getattr(request.user, 'profile', None) - is_manager = False - try: - is_manager = bool(profile and profile.has_role(UserRoles.MANAGER)) - except Exception: - is_manager = False - if request.method == 'POST': - if not is_manager: - messages.error(request, 'شما اجازه تعیین نصاب را ندارید') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) installer_id = request.POST.get('installer_id') scheduled_date = (request.POST.get('scheduled_date') or '').strip() assignment.installer_id = installer_id or None @@ -55,10 +43,6 @@ def installation_assign_step(request, instance_id, step_id): return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) return redirect('processes:request_list') - # Read-only logic for non-managers - read_only = not is_manager - show_denied_msg = (not is_manager) and (assignment.installer_id is None) - return render(request, 'installations/installation_assign_step.html', { 'instance': instance, 'step': step, @@ -66,9 +50,6 @@ def installation_assign_step(request, instance_id, step_id): 'installers': installers, 'previous_step': previous_step, 'next_step': next_step, - 'is_manager': is_manager, - 'read_only': read_only, - 'show_denied_msg': show_denied_msg, }) @@ -80,94 +61,15 @@ def installation_report_step(request, instance_id, step_id): next_step = instance.process.steps.filter(order__gt=step.order).first() assignment = InstallationAssignment.objects.filter(process_instance=instance).first() existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first() - # Only installers can enter edit mode - user_is_installer = hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.INSTALLER) - edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False + edit_mode = True if request.GET.get('edit') == '1' else False + print("edit_mode", edit_mode) # current quote items baseline quote = Quote.objects.filter(process_instance=instance).first() quote_items = list(quote.items.select_related('item').all()) if quote else [] quote_price_map = {qi.item_id: qi.unit_price for qi in quote_items} - items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name') - - # Ensure a StepInstance exists for this step - step_instance, _ = StepInstance.objects.get_or_create( - process_instance=instance, - step=step, - defaults={'status': 'in_progress'} - ) - - # Build approver requirements/status for UI - reqs = list(step.approver_requirements.select_related('role').all()) - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None) - user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else [] - user_can_approve = any(r.role in user_roles for r in reqs) - approvals_list = list(step_instance.approvals.select_related('role').all()) - approvals_by_role = {a.role_id: a for a in approvals_list} - approver_statuses = [ - { - 'role': r.role, - 'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None), - 'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''), - } - for r in reqs - ] - - # Manager approval/rejection actions - if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']: - action = request.POST.get('action') - # find a matching approver role based on step requirements - req_roles = [req.role for req in step.approver_requirements.select_related('role').all()] - user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all()) - matching_role = next((r for r in user_roles if r in req_roles), None) - if matching_role is None: - messages.error(request, 'شما دسترسی لازم برای این عملیات را ندارید.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) - - if not existing_report: - messages.error(request, 'گزارش برای تایید/رد وجود ندارد.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) - - if action == 'approve': - existing_report.approved = True - existing_report.save() - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} - ) - if step_instance.is_fully_approved(): - step_instance.status = 'completed' - step_instance.completed_at = timezone.now() - step_instance.save() - if next_step: - instance.current_step = next_step - instance.save() - return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) - return redirect('processes:request_list') - messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقش‌ها.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) - - if action == 'reject': - reason = (request.POST.get('reject_reason') or '').strip() - if not reason: - messages.error(request, 'لطفاً علت رد شدن را وارد کنید.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} - ) - StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason) - existing_report.approved = False - existing_report.save() - messages.success(request, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) + items = Item.objects.all().order_by('name') if request.method == 'POST': - # Only installers can submit or edit reports (non-approval actions) - if request.POST.get('action') not in ['approve', 'reject'] and not user_is_installer: - messages.error(request, 'شما مجوز ثبت/ویرایش گزارش نصب را ندارید') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) description = (request.POST.get('description') or '').strip() visited_date = (request.POST.get('visited_date') or '').strip() if '/' in visited_date: @@ -232,7 +134,6 @@ def installation_report_step(request, instance_id, step_id): report.is_meter_suspicious = is_suspicious report.utm_x = utm_x report.utm_y = utm_y - report.approved = False # back to awaiting approval after edits report.save() # delete selected existing photos for key, val in request.POST.items(): @@ -310,17 +211,18 @@ def installation_report_step(request, instance_id, step_id): total_price=total, ) - # After installer submits/edits, set step back to in_progress and clear approvals - step_instance.status = 'in_progress' - step_instance.completed_at = None - step_instance.save() - try: - step_instance.approvals.all().delete() - except Exception: - pass + # complete step + StepInstance.objects.update_or_create( + process_instance=instance, + step=step, + defaults={'status': 'completed', 'completed_at': timezone.now()} + ) - messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.') - return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id) + if next_step: + instance.current_step = next_step + instance.save() + return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) + return redirect('processes:request_list') # Build prefill maps from existing report changes removed_ids = set() @@ -348,9 +250,6 @@ def installation_report_step(request, instance_id, step_id): 'added_map': added_map, 'previous_step': previous_step, 'next_step': next_step, - 'step_instance': step_instance, - 'approver_statuses': approver_statuses, - 'user_can_approve': user_can_approve, }) diff --git a/invoices/admin.py b/invoices/admin.py index f8a46cb..8428a2a 100644 --- a/invoices/admin.py +++ b/invoices/admin.py @@ -6,8 +6,8 @@ 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_special', 'is_active', 'created_by'] - list_filter = ['is_default_in_quotes', 'is_special', 'is_active', 'created_by'] + 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'] diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py index 1714b64..abf23d3 100644 --- a/invoices/migrations/0001_initial.py +++ b/invoices/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-14 09:02 import django.db.models.deletion import simple_history.models @@ -29,7 +29,6 @@ class Migration(migrations.Migration): ('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='قیمت واحد')), - ('is_special', models.BooleanField(default=False, 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)), @@ -122,12 +121,10 @@ class Migration(migrations.Migration): ('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='مبلغ پرداخت')), - ('direction', models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, 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, db_index=True, max_length=100, 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ها')), - ('receipt_image', models.TextField(blank=True, max_length=100, null=True, 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)), @@ -157,7 +154,6 @@ class Migration(migrations.Migration): ('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='قیمت واحد')), - ('is_special', models.BooleanField(default=False, 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='ایجاد کننده')), @@ -229,12 +225,10 @@ class Migration(migrations.Migration): ('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='مبلغ پرداخت')), - ('direction', models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, 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, unique=True, 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ها')), - ('receipt_image', models.ImageField(blank=True, null=True, upload_to='payments/%Y/%m/%d/', verbose_name='تصویر فیش')), ('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='فاکتور')), ], diff --git a/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py b/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py new file mode 100644 index 0000000..d8de4a0 --- /dev/null +++ b/invoices/migrations/0002_historicalpayment_receipt_image_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-08-16 04:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='historicalpayment', + name='receipt_image', + field=models.TextField(blank=True, max_length=100, null=True, verbose_name='تصویر فیش'), + ), + migrations.AddField( + model_name='payment', + name='receipt_image', + field=models.ImageField(blank=True, null=True, upload_to='payments/%Y/%m/%d/', verbose_name='تصویر فیش'), + ), + ] diff --git a/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py b/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py new file mode 100644 index 0000000..d84cc14 --- /dev/null +++ b/invoices/migrations/0003_alter_historicalpayment_reference_number_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-08-21 18:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0002_historicalpayment_receipt_image_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalpayment', + name='reference_number', + field=models.CharField(blank=True, db_index=True, max_length=100, verbose_name='شماره مرجع'), + ), + migrations.AlterField( + model_name='payment', + name='reference_number', + field=models.CharField(blank=True, max_length=100, unique=True, verbose_name='شماره مرجع'), + ), + ] diff --git a/invoices/migrations/0004_historicalpayment_direction_payment_direction.py b/invoices/migrations/0004_historicalpayment_direction_payment_direction.py new file mode 100644 index 0000000..e3416cc --- /dev/null +++ b/invoices/migrations/0004_historicalpayment_direction_payment_direction.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-08-22 08:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0003_alter_historicalpayment_reference_number_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='historicalpayment', + name='direction', + field=models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش'), + ), + migrations.AddField( + model_name='payment', + name='direction', + field=models.CharField(choices=[('in', 'دریافتی'), ('out', 'پرداختی')], default='in', max_length=3, verbose_name='نوع تراکنش'), + ), + ] diff --git a/invoices/migrations/0005_historicalitem_is_special_and_more.py b/invoices/migrations/0005_historicalitem_is_special_and_more.py new file mode 100644 index 0000000..7524867 --- /dev/null +++ b/invoices/migrations/0005_historicalitem_is_special_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.4 on 2025-08-22 08:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0004_historicalpayment_direction_payment_direction'), + ] + + operations = [ + migrations.AddField( + model_name='historicalitem', + name='is_special', + field=models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی'), + ), + migrations.AddField( + model_name='historicalitem', + name='special_kind', + field=models.CharField(blank=True, choices=[('repair', 'تعمیر'), ('replace', 'تعویض')], max_length=10, verbose_name='نوع ویژه'), + ), + migrations.AddField( + model_name='item', + name='is_special', + field=models.BooleanField(default=False, verbose_name='ویژه برای فاکتور نهایی'), + ), + migrations.AddField( + model_name='item', + name='special_kind', + field=models.CharField(blank=True, choices=[('repair', 'تعمیر'), ('replace', 'تعویض')], max_length=10, verbose_name='نوع ویژه'), + ), + ] diff --git a/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py b/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py new file mode 100644 index 0000000..5abdd01 --- /dev/null +++ b/invoices/migrations/0006_remove_historicalitem_special_kind_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-08-22 08:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0005_historicalitem_is_special_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='historicalitem', + name='special_kind', + ), + migrations.RemoveField( + model_name='item', + name='special_kind', + ), + ] diff --git a/invoices/templates/invoices/final_invoice_step.html b/invoices/templates/invoices/final_invoice_step.html index dfee339..9376705 100644 --- a/invoices/templates/invoices/final_invoice_step.html +++ b/invoices/templates/invoices/final_invoice_step.html @@ -50,9 +50,7 @@
فاکتور نهایی
- {% if is_manager %} - - {% endif %} +
@@ -129,9 +127,7 @@ {{ si.unit_price|floatformat:0|intcomma:False }} {{ si.total_price|floatformat:0|intcomma:False }} - {% if is_manager %} - - {% endif %} + {% endfor %} @@ -168,11 +164,7 @@ {% endif %} {% if next_step %} - {% if is_manager %} - - {% else %} - بعدی - {% endif %} + {% endif %}
diff --git a/invoices/templates/invoices/final_settlement_step.html b/invoices/templates/invoices/final_settlement_step.html index ca11dec..5058a09 100644 --- a/invoices/templates/invoices/final_settlement_step.html +++ b/invoices/templates/invoices/final_settlement_step.html @@ -2,7 +2,6 @@ {% load static %} {% load processes_tags %} {% load common_tags %} -{% load accounts_tags %} {% load humanize %} {% block sidebar %} @@ -47,7 +46,6 @@
- {% if is_broker %}
ثبت تراکنش تسویه
@@ -80,11 +78,11 @@
- +
- +
@@ -94,39 +92,23 @@
- {% endif %} -
+
-
-
وضعیت فاکتور
-
+
وضعیت فاکتور
-
-
+
+
مبلغ نهایی
{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان
-
-
-
پرداختی‌ها
-
{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان
-
-
-
-
+
+
مانده
{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
-
- {% if invoice.remaining_amount <= 0 %} - تسویه کامل - {% else %} - باقی‌مانده دارد - {% endif %} -
@@ -141,8 +123,8 @@ مبلغ تاریخ روش - شماره مرجع/چک - عملیات + شماره مرجع + عملیات @@ -150,7 +132,7 @@ {% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %} {{ p.amount|floatformat:0|intcomma:False }} تومان - {{ p.payment_date|date:'Y/m/d' }} + {{ p.payment_date|to_jalali }} {{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} @@ -160,9 +142,7 @@ {% endif %} - {% if is_broker %} - - {% endif %} +
@@ -172,141 +152,20 @@
- -
-
-
- {% if approver_statuses %} -
-
-
وضعیت تاییدها
- {% if can_approve_reject %} -
- - -
- {% endif %} -
-
-
- {% for st in approver_statuses %} -
-
-
- {{ st.role.name }} - {% if st.status == 'approved' %} - تایید شد - {% elif st.status == 'rejected' %} - رد شد - {% else %} - در انتظار - {% endif %} -
- {% if st.status == 'rejected' and st.reason %} -
علت: {{ st.reason }}
- {% endif %} -
+ - {% endfor %}
- {% endif %} -
- {% if previous_step %} - قبلی - {% else %} - - {% endif %} - {% if step_instance.status == 'completed' %} - {% if next_step %} - بعدی - {% else %} - اتمام - {% endif %} - {% endif %} -
- - - - - - - - - - - - {% endblock %} {% block script %} @@ -332,11 +191,8 @@ if (g) { fd.set('payment_date', g); } return fd; } - (function(){ - const btn = document.getElementById('btnAddFinalPayment'); - if (!btn) return; - btn.addEventListener('click', function(){ - const fd = buildForm(); + document.getElementById('btnAddFinalPayment').addEventListener('click', function(){ + const fd = buildForm(); // Frontend validation const amount = document.getElementById('id_amount').value.trim(); const payDate = document.getElementById('id_payment_date').value.trim(); @@ -348,7 +204,7 @@ showToast('همه فیلدها الزامی است', 'danger'); return; } - fetch('{% url "invoices:add_final_payment" instance.id step.id %}', { method:'POST', body: fd }) + fetch('{% url "invoices:add_final_payment" instance.id step.id %}', { method:'POST', body: fd }) .then(r=>r.json()).then(resp=>{ if (resp.success) { showToast('تراکنش ثبت شد', 'success'); @@ -357,20 +213,12 @@ showToast(resp.message || 'خطا در ثبت تراکنش', 'danger'); } }).catch(()=> showToast('خطا در ارتباط با سرور', 'danger')); - }); - })(); + }); - let deleteTargetId = null; - function openDeleteModal(id){ - deleteTargetId = id; - const modal = new bootstrap.Modal(document.getElementById('deletePaymentModal')); - modal.show(); - } - function confirmDeletePayment(){ - if (!deleteTargetId) return; + function deleteFinalPayment(id){ const fd = new FormData(); fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value); - fetch(`{% url "invoices:delete_final_payment" instance.id step.id 0 %}`.replace('/0/', `/${deleteTargetId}/`), { method:'POST', body: fd }) + fetch(`{% url "invoices:delete_final_payment" instance.id step.id 0 %}`.replace('/0/', `/${id}/`), { method:'POST', body: fd }) .then(r=>r.json()).then(resp=>{ if (resp.success) { showToast('حذف شد', 'success'); @@ -381,7 +229,20 @@ }).catch(()=> showToast('خطا در ارتباط با سرور', 'danger')); } - // Legacy approve button removed; using modal forms below + document.getElementById('btnApproveFinalSettlement').addEventListener('click', function(){ + const fd = new FormData(); + fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value); + fetch('{% url "invoices:approve_final_settlement" instance.id step.id %}', { method:'POST', body: fd }) + .then(r=>r.json()).then(resp=>{ + if (resp.success) { + showToast(resp.message || 'تایید شد', 'success'); + if (resp.redirect) setTimeout(()=>{ window.location.href = resp.redirect; }, 600); + } else { + showToast(resp.message || 'خطا در تایید', 'danger'); + } + }).catch(()=> showToast('خطا در ارتباط با سرور', 'danger')); + }); {% endblock %} + diff --git a/invoices/templates/invoices/quote_payment_step.html b/invoices/templates/invoices/quote_payment_step.html index 1b963ee..ab9f518 100644 --- a/invoices/templates/invoices/quote_payment_step.html +++ b/invoices/templates/invoices/quote_payment_step.html @@ -1,7 +1,6 @@ {% extends '_base.html' %} {% load static %} {% load processes_tags %} -{% load accounts_tags %} {% load humanize %} {% block sidebar %} @@ -56,15 +55,14 @@
{{ step.name }}
- ثبت فیش‌ها/چک‌های واریزی برای پیش‌فاکتور + ثبت فیش‌های واریزی برای پیش‌فاکتور
- {% if can_manage_payments %}
-
ثبت فیش/چک جدید
+
ثبت فیش جدید
@@ -86,11 +84,11 @@
- +
- +
@@ -98,16 +96,16 @@
- +
- {% endif %} -
+
-
وضعیت پیش‌فاکتور
+
وضعیت پیش‌فاکتور
+ مشاهده پیش‌فاکتور
@@ -141,10 +139,8 @@
-
-
-
فیش‌ها/چک‌های ثبت شده
-
+
+
فیش‌های ثبت شده
@@ -153,8 +149,9 @@ - - + + + @@ -165,23 +162,28 @@ + {% empty %} - + {% endfor %} @@ -189,42 +191,6 @@ - {% if approver_statuses %} -
-
-
وضعیت تاییدها
- {% if can_approve_reject %} -
- - -
- {% endif %} -
-
-
- {% for st in approver_statuses %} -
-
-
- {{ st.role.name }} - {% if st.status == 'approved' %} - تایید شد - {% elif st.status == 'rejected' %} - رد شد - {% else %} - در انتظار - {% endif %} -
- {% if st.status == 'rejected' and st.reason %} -
علت: {{ st.reason }}
- {% endif %} -
-
- {% endfor %} -
-
-
- {% endif %}
{% if previous_step %} @@ -235,114 +201,30 @@ {% else %} {% endif %} - {% if step_instance.status == 'completed' %} - {% if next_step %} - - بعدی - - - {% else %} - اتمام - {% endif %} - {% endif %} +
- - - - - - - - - - {% endblock %} {% block script %} @@ -439,4 +365,42 @@ })(); + + + + {% endblock %} diff --git a/invoices/templates/invoices/quote_preview_step.html b/invoices/templates/invoices/quote_preview_step.html index e6e405a..4ba69f1 100644 --- a/invoices/templates/invoices/quote_preview_step.html +++ b/invoices/templates/invoices/quote_preview_step.html @@ -221,32 +221,20 @@ {% endif %} - {% if is_broker %} - {% if step_instance.status == 'completed' %} - {% if next_step %} - - بعدی - - - {% else %} - - {% endif %} - {% else %} - - {% endif %} - {% else %} + {% if step_instance.status == 'completed' %} {% if next_step %} - مرحله بعد + class="btn btn-primary"> + بعدی {% else %} - اتمام + {% endif %} + {% else %} + {% endif %} diff --git a/invoices/templates/invoices/quote_step.html b/invoices/templates/invoices/quote_step.html index ca17747..5219c40 100644 --- a/invoices/templates/invoices/quote_step.html +++ b/invoices/templates/invoices/quote_step.html @@ -58,7 +58,6 @@ {% endif %}
- {% if is_broker or existing_quote %}
مبلغ تاریخ روششماره مرجع/چکعملیاتشماره مرجعتصویرعملیات
{{ p.get_payment_method_display }} {{ p.reference_number|default:'-' }} -
- {% if p.receipt_image %} + {% if p.receipt_image %} - {% endif %} - {% if can_manage_payments %} -
+
+ + - {% endif %}
تا کنون فیش/چکی ثبت نشده استتا کنون فیشی ثبت نشده است
@@ -78,8 +77,7 @@ data-item-id="{{ item.id }}" data-is-default="{% if item.is_default_in_quotes %}1{% else %}0{% endif %}" {% if selected_qty %}checked{% elif item.is_default_in_quotes %}checked{% endif %} - {% if item.is_default_in_quotes or not is_broker %}disabled{% endif %} - {% if item.is_default_in_quotes %}title="آیتم پیش‌فرض است و قابل حذف نیست"{% elif not is_broker %}title="فقط کارگزار مجاز به تغییر اقلام است"{% endif %}> + {% if item.is_default_in_quotes %}disabled title="آیتم پیش‌فرض است و قابل حذف نیست"{% endif %}> {% endwith %} @@ -104,9 +102,8 @@
@@ -88,15 +86,15 @@ پیش‌فرض {% endif %} + {% if item.description %}{{ item.description }}{% endif %}
{{ item.unit_price|floatformat:0|intcomma:False }} تومان - +
- {% else %} -
شما دسترسی به ثبت اقلام ندارید.
- {% endif %} + +
@@ -121,35 +118,27 @@ {% endif %} - {% if is_broker %} - {% if step_instance.status == 'completed' %} - {% if next_step %} -
- -
- {% else %} - - {% endif %} + {% if step_instance.status == 'completed' %} + {% if next_step %} +
+ +
+ {% else %} - + {% endif %} {% else %} - {% if next_step %} - - مرحله بعد - - - {% else %} - اتمام - {% endif %} + {% endif %}
diff --git a/invoices/views.py b/invoices/views.py index 6429499..b39aafe 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -9,9 +9,7 @@ from django.urls import reverse from decimal import Decimal, InvalidOperation import json -from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval -from accounts.models import Role -from common.consts import UserRoles +from processes.models import ProcessInstance, ProcessStep, StepInstance from .models import Item, Quote, QuoteItem, Payment, Invoice from installations.models import InstallationReport, InstallationItemChange @@ -30,7 +28,7 @@ def quote_step(request, instance_id, step_id): return redirect('processes:request_list') # دریافت آیتم‌ها - items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name') + items = Item.objects.all().order_by('name') existing_quote = Quote.objects.filter(process_instance=instance).first() existing_quote_items = {} if existing_quote: @@ -42,14 +40,6 @@ def quote_step(request, instance_id, step_id): previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() - # determine if current user is broker - profile = getattr(request.user, 'profile', None) - is_broker = False - try: - is_broker = bool(profile and profile.has_role(UserRoles.BROKER)) - except Exception: - is_broker = False - return render(request, 'invoices/quote_step.html', { 'instance': instance, 'step': step, @@ -59,7 +49,6 @@ def quote_step(request, instance_id, step_id): 'existing_quote': existing_quote, 'previous_step': previous_step, 'next_step': next_step, - 'is_broker': is_broker, }) @require_POST @@ -68,13 +57,6 @@ def create_quote(request, instance_id, step_id): """ساخت/بروزرسانی پیش‌فاکتور از اقلام انتخابی""" instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) - # enforce permission: only BROKER can create/update quote - profile = getattr(request.user, 'profile', None) - try: - if not (profile and profile.has_role(UserRoles.BROKER)): - return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیش‌فاکتور را ندارید'}) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز ثبت/ویرایش پیش‌فاکتور را ندارید'}) try: items_payload = json.loads(request.POST.get('items') or '[]') @@ -90,7 +72,7 @@ def create_quote(request, instance_id, step_id): except Exception: continue - default_item_ids = set(Item.objects.filter(is_default_in_quotes=True, is_deleted=False).values_list('id', flat=True)) + default_item_ids = set(Item.objects.filter(is_default_in_quotes=True).values_list('id', flat=True)) if default_item_ids: for default_id in default_item_ids: if default_id not in payload_by_id: @@ -181,14 +163,6 @@ def quote_preview_step(request, instance_id, step_id): previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() - # determine if current user is broker for UI controls - profile = getattr(request.user, 'profile', None) - is_broker = False - try: - is_broker = bool(profile and profile.has_role(UserRoles.BROKER)) - except Exception: - is_broker = False - return render(request, 'invoices/quote_preview_step.html', { 'instance': instance, 'step': step, @@ -196,7 +170,6 @@ def quote_preview_step(request, instance_id, step_id): 'quote': quote, 'previous_step': previous_step, 'next_step': next_step, - 'is_broker': is_broker, }) @login_required @@ -217,13 +190,6 @@ def approve_quote(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) - # enforce permission: only BROKER can approve - profile = getattr(request.user, 'profile', None) - try: - if not (profile and profile.has_role(UserRoles.BROKER)): - return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیش‌فاکتور را ندارید'}) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز تایید پیش‌فاکتور را ندارید'}) # تایید پیش‌فاکتور quote.status = 'sent' @@ -281,97 +247,7 @@ def quote_payment_step(request, instance_id, step_id): 'is_fully_paid': quote.get_remaining_amount() <= 0, } - step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'}) - reqs = list(step.approver_requirements.select_related('role').all()) - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None) - user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else [] - approvals_list = list(step_instance.approvals.select_related('role').all()) - approvals_by_role = {a.role_id: a for a in approvals_list} - approver_statuses = [ - { - 'role': r.role, - 'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None), - 'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''), - } - for r in reqs - ] - # dynamic permission: who can approve/reject this step (based on requirements) - try: - req_role_ids = {r.role_id for r in reqs} - user_role_ids = {ur.id for ur in user_roles} - can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0 - except Exception: - can_approve_reject = False - # approver status map for template - reqs = list(step.approver_requirements.select_related('role').all()) - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None) - user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else [] - approvals_list = list(step_instance.approvals.select_related('role').all()) - approvals_by_role = {a.role_id: a for a in approvals_list} - approver_statuses = [ - { - 'role': r.role, - 'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None), - 'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''), - } - for r in reqs - ] - - # Accountant/Admin approval and rejection via POST (multi-role) - if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']: - # match user's role against step required approver roles - req_roles = [req.role for req in step.approver_requirements.select_related('role').all()] - user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all()) - matching_role = next((r for r in user_roles if r in req_roles), None) - if matching_role is None: - messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.') - return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) - - action = request.POST.get('action') - if action == 'approve': - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} - ) - if step_instance.is_fully_approved(): - step_instance.status = 'completed' - step_instance.completed_at = timezone.now() - step_instance.save() - # move to next step - redirect_url = 'processes:request_list' - if next_step: - instance.current_step = next_step - instance.save() - return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) - return redirect(redirect_url) - messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقش‌ها.') - return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) - - if action == 'reject': - reason = (request.POST.get('reject_reason') or '').strip() - if not reason: - messages.error(request, 'علت رد شدن را وارد کنید') - return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} - ) - StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason) - messages.success(request, 'مرحله پرداخت‌ها رد شد و برای اصلاح بازگشت.') - return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id) - - # role flags for permissions (legacy flags kept for compatibility) - profile = getattr(request.user, 'profile', None) - is_broker = False - is_accountant = False - try: - is_broker = bool(profile and profile.has_role(UserRoles.BROKER)) - is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT)) - except Exception: - is_broker = False - is_accountant = False + step_instance = instance.step_instances.filter(step=step).first() return render(request, 'invoices/quote_payment_step.html', { 'instance': instance, @@ -382,12 +258,6 @@ def quote_payment_step(request, instance_id, step_id): 'totals': totals, 'previous_step': previous_step, 'next_step': next_step, - 'approver_statuses': approver_statuses, - 'is_broker': is_broker, - 'is_accountant': is_accountant, - # dynamic permissions: any role required to approve can also manage payments - 'can_manage_payments': can_approve_reject, - 'can_approve_reject': can_approve_reject, }) @@ -409,16 +279,6 @@ def add_quote_payment(request, instance_id, step_id): } ) - # dynamic permission: users whose roles are among required approvers can add payments - try: - req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True)) - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()) - user_role_ids = set(user_roles_qs.values_list('id', flat=True)) - if len(req_role_ids.intersection(user_role_ids)) == 0: - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'}) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'}) - logger = logging.getLogger(__name__) try: amount = (request.POST.get('amount') or '').strip() @@ -465,15 +325,6 @@ def add_quote_payment(request, instance_id, step_id): logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id) return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)}) - # After modifying payments, set step back to in_progress (awaiting approval) - try: - si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) - si.status = 'in_progress' - si.completed_at = None - si.save() - si.approvals.all().delete() - except Exception: - pass redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @@ -509,15 +360,6 @@ def update_quote_payment(request, instance_id, step_id, payment_id): except Exception: return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'}) - # On update, return to awaiting approval - try: - si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) - si.status = 'in_progress' - si.completed_at = None - si.save() - si.approvals.all().delete() - except Exception: - pass redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @@ -532,30 +374,11 @@ def delete_quote_payment(request, instance_id, step_id, payment_id): if not invoice: return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'}) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) - # dynamic permission: users whose roles are among required approvers can delete payments - try: - req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True)) - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()) - user_role_ids = set(user_roles_qs.values_list('id', flat=True)) - if len(req_role_ids.intersection(user_role_ids)) == 0: - return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'}) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'}) - try: # soft delete using project's BaseModel delete override payment.delete() except Exception: return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'}) - # On delete, return to awaiting approval - try: - si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) - si.status = 'in_progress' - si.completed_at = None - si.save() - si.approvals.all().delete() - except Exception: - pass redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id]) return JsonResponse({'success': True, 'redirect': redirect_url}) @@ -711,14 +534,6 @@ def final_invoice_step(request, instance_id, step_id): # Choices for special items from DB special_choices = list(Item.objects.filter(is_special=True).values('id', 'name')) - # role flag for manager-only actions - profile = getattr(request.user, 'profile', None) - is_manager = False - try: - is_manager = bool(profile and profile.has_role(UserRoles.MANAGER)) - except Exception: - is_manager = False - return render(request, 'invoices/final_invoice_step.html', { 'instance': instance, 'step': step, @@ -728,7 +543,6 @@ def final_invoice_step(request, instance_id, step_id): 'invoice_specials': invoice.items.select_related('item').filter(item__is_special=True, is_deleted=False).all(), 'previous_step': previous_step, 'next_step': next_step, - 'is_manager': is_manager, }) @@ -750,12 +564,6 @@ def approve_final_invoice(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) - # only MANAGER can approve - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)): - return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403) # Block approval when there is any remaining (positive or negative) invoice.calculate_totals() # if invoice.remaining_amount != 0: @@ -784,12 +592,6 @@ def add_special_charge(request, instance_id, step_id): """افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی به‌صورت آیتم جداگانه""" instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) - # only MANAGER can add special charges - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)): - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن هزینه ویژه را ندارید'}, status=403) # charge_type was removed from UI; we no longer require it item_id = request.POST.get('item_id') amount = (request.POST.get('amount') or '').strip() @@ -821,12 +623,6 @@ def add_special_charge(request, instance_id, step_id): def delete_special_charge(request, instance_id, step_id, item_id): instance = get_object_or_404(ProcessInstance, id=instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) - # only MANAGER can delete special charges - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.MANAGER)): - return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز حذف هزینه ویژه را ندارید'}, status=403) from .models import InvoiceItem inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice) # allow deletion only for special items @@ -852,87 +648,13 @@ def final_settlement_step(request, instance_id, step_id): previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() - # Ensure step instance exists - step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'}) - # Build approver statuses for template - reqs = list(step.approver_requirements.select_related('role').all()) - approvals_map = {a.role_id: a.decision for a in step_instance.approvals.select_related('role').all()} - approver_statuses = [{'role': r.role, 'status': approvals_map.get(r.role_id)} for r in reqs] - # dynamic permission to control approve/reject UI - try: - user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()) - user_role_ids = set(user_roles_qs.values_list('id', flat=True)) - req_role_ids = {r.role_id for r in reqs} - can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0 - except Exception: - can_approve_reject = False - - # Accountant/Admin approval and rejection (multi-role) - if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']: - req_roles = [req.role for req in step.approver_requirements.select_related('role').all()] - user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all()) - matching_role = next((r for r in user_roles if r in req_roles), None) - if matching_role is None: - messages.error(request, 'شما دسترسی لازم برای تایید/رد این مرحله را ندارید.') - return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) - - action = request.POST.get('action') - if action == 'approve': - # enforce zero remaining - invoice.calculate_totals() - if invoice.remaining_amount != 0: - messages.error(request, f"تا زمانی که مانده فاکتور صفر نشده امکان تایید نیست (مانده فعلی: {invoice.remaining_amount})") - return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''} - ) - if step_instance.is_fully_approved(): - step_instance.status = 'completed' - step_instance.completed_at = timezone.now() - step_instance.save() - if next_step: - instance.current_step = next_step - instance.save() - return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id) - return redirect('processes:request_list') - messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقش‌ها.') - return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) - - if action == 'reject': - reason = (request.POST.get('reject_reason') or '').strip() - if not reason: - messages.error(request, 'علت رد شدن را وارد کنید') - return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) - StepApproval.objects.update_or_create( - step_instance=step_instance, - role=matching_role, - defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} - ) - StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason) - messages.success(request, 'مرحله تسویه نهایی رد شد و برای اصلاح بازگشت.') - return redirect('invoices:final_settlement_step', instance_id=instance.id, step_id=step.id) - - # broker flag for payment management permission - profile = getattr(request.user, 'profile', None) - is_broker = False - try: - is_broker = bool(profile and profile.has_role(UserRoles.BROKER)) - except Exception: - is_broker = False - return render(request, 'invoices/final_settlement_step.html', { 'instance': instance, 'step': step, 'invoice': invoice, 'payments': invoice.payments.filter(is_deleted=False).all(), - 'step_instance': step_instance, 'previous_step': previous_step, 'next_step': next_step, - 'approver_statuses': approver_statuses, - 'can_approve_reject': can_approve_reject, - 'is_broker': is_broker, }) @@ -940,14 +662,7 @@ def final_settlement_step(request, instance_id, step_id): @login_required def add_final_payment(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=instance_id) - step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) - # Only BROKER can add final settlement payments - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)): - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز افزودن تراکنش تسویه را ندارید'}, status=403) amount = (request.POST.get('amount') or '').strip() payment_date = (request.POST.get('payment_date') or '').strip() payment_method = (request.POST.get('payment_method') or '').strip() @@ -1002,14 +717,6 @@ def add_final_payment(request, instance_id, step_id): ) # After creation, totals auto-updated by model save. Respond with redirect and new totals for UX. invoice.refresh_from_db() - # After payment change, set step back to in_progress - try: - si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) - si.status = 'in_progress' - si.completed_at = None - si.save() - except Exception: - pass return JsonResponse({ 'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), @@ -1025,25 +732,10 @@ def add_final_payment(request, instance_id, step_id): @login_required def delete_final_payment(request, instance_id, step_id, payment_id): instance = get_object_or_404(ProcessInstance, id=instance_id) - step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) - # Only BROKER can delete final settlement payments - try: - if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)): - return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403) - except Exception: - return JsonResponse({'success': False, 'message': 'شما مجوز حذف تراکنش تسویه را ندارید'}, status=403) payment.delete() invoice.refresh_from_db() - # After payment change, set step back to in_progress - try: - si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step) - si.status = 'in_progress' - si.completed_at = None - si.save() - except Exception: - pass return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': { 'final_amount': str(invoice.final_amount), 'paid_amount': str(invoice.paid_amount), diff --git a/locations/migrations/0001_initial.py b/locations/migrations/0001_initial.py index db3cdac..def46a0 100644 --- a/locations/migrations/0001_initial.py +++ b/locations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-14 09:02 import django.db.models.deletion from django.db import migrations, models diff --git a/processes/admin.py b/processes/admin.py index 0cbe11f..4c83c14 100644 --- a/processes/admin.py +++ b/processes/admin.py @@ -2,7 +2,7 @@ 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 Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepApproverRequirement, StepApproval +from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision @admin.register(Process) class ProcessAdmin(SimpleHistoryAdmin): @@ -168,16 +168,14 @@ class StepRejectionAdmin(SimpleHistoryAdmin): return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason reason_short.short_description = "دلیل رد شدن" +@admin.register(StepRevision) +class StepRevisionAdmin(SimpleHistoryAdmin): + list_display = ['step_instance', 'rejection', 'revised_by', 'changes_short', 'created_at'] + list_filter = ['revised_by', 'created_at', 'step_instance__step__process'] + search_fields = ['step_instance__step__name', 'revised_by__username', 'changes_description'] + readonly_fields = ['created_at'] + ordering = ['-created_at'] -@admin.register(StepApproverRequirement) -class StepApproverRequirementAdmin(admin.ModelAdmin): - list_display = ("step", "role", "required_count") - list_filter = ("step__process", "role") - search_fields = ("step__name", "role__name") - - -@admin.register(StepApproval) -class StepApprovalAdmin(admin.ModelAdmin): - list_display = ("step_instance", "role", "decision", "approved_by", "created_at") - list_filter = ("decision", "role", "step_instance__step__process") - search_fields = ("step_instance__process_instance__code", "role__name", "approved_by__username") + def changes_short(self, obj): + return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description + changes_short.short_description = "تغییرات" diff --git a/processes/forms.py b/processes/forms.py index e69de29..534c475 100644 --- a/processes/forms.py +++ b/processes/forms.py @@ -0,0 +1,26 @@ +from django import forms +from .models import ProcessInstance, StepInstance + +class ProcessInstanceForm(forms.ModelForm): + class Meta: + model = ProcessInstance + fields = ['description', 'process', 'well', 'representative', 'requester', 'priority', 'status', 'current_step'] + widgets = { + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'process': forms.Select(attrs={'class': 'form-control'}), + 'well': forms.Select(attrs={'class': 'form-control'}), + 'representative': forms.Select(attrs={'class': 'form-control'}), + 'requester': forms.Select(attrs={'class': 'form-control'}), + 'priority': forms.Select(attrs={'class': 'form-control'}), + 'status': forms.Select(attrs={'class': 'form-control'}), + 'current_step': forms.Select(attrs={'class': 'form-control'}), + } + +class StepInstanceForm(forms.ModelForm): + class Meta: + model = StepInstance + fields = ['status', 'notes'] + widgets = { + 'status': forms.Select(attrs={'class': 'form-control'}), + 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}) + } \ No newline at end of file diff --git a/processes/migrations/0001_initial.py b/processes/migrations/0001_initial.py index 4e90bf7..8675e70 100644 --- a/processes/migrations/0001_initial.py +++ b/processes/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-09-07 07:35 +# Generated by Django 5.2.4 on 2025-08-14 09:02 import django.db.models.deletion import simple_history.models @@ -11,7 +11,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('accounts', '0001_initial'), ('wells', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -232,17 +231,42 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='StepApproverRequirement', + name='HistoricalStepRevision', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('required_count', models.PositiveIntegerField(default=1, verbose_name='تعداد موردنیاز')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش تاییدکننده')), - ('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approver_requirements', to='processes.processstep', verbose_name='مرحله')), + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')), + ('created_at', models.DateTimeField(blank=True, editable=False, 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)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('revised_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='اصلاح کننده')), + ('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')), + ('rejection', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.steprejection', verbose_name='رد شدن مربوطه')), ], options={ - 'verbose_name': 'نیازمندی تایید نقش', - 'verbose_name_plural': 'نیازمندی\u200cهای تایید نقش', - 'unique_together': {('step', 'role')}, + '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='StepRevision', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ اصلاح')), + ('rejection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.steprejection', verbose_name='رد شدن مربوطه')), + ('revised_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_revisions', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')), + ('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.stepinstance', verbose_name='نمونه مرحله')), + ], + options={ + 'verbose_name': 'بازبینی مرحله', + 'verbose_name_plural': 'بازبینی\u200cهای مراحل', + 'ordering': ['-created_at'], }, ), migrations.CreateModel( @@ -260,21 +284,4 @@ class Migration(migrations.Migration): 'unique_together': {('dependent_step', 'dependency_step')}, }, ), - migrations.CreateModel( - name='StepApproval', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('decision', models.CharField(choices=[('approved', 'تایید'), ('rejected', 'رد')], max_length=8, verbose_name='نتیجه')), - ('reason', models.TextField(blank=True, verbose_name='علت (برای رد)')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ')), - ('approved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='تاییدکننده')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش')), - ('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approvals', to='processes.stepinstance', verbose_name='نمونه مرحله')), - ], - options={ - 'verbose_name': 'تایید مرحله', - 'verbose_name_plural': 'تاییدهای مرحله', - 'unique_together': {('step_instance', 'role')}, - }, - ), ] diff --git a/processes/models.py b/processes/models.py index 0a119db..cfbdfa7 100644 --- a/processes/models.py +++ b/processes/models.py @@ -4,8 +4,6 @@ from common.models import NameSlugModel, SluggedModel from simple_history.models import HistoricalRecords from django.core.exceptions import ValidationError from django.utils import timezone -from django.conf import settings -from accounts.models import Role from _helpers.utils import generate_unique_slug import random @@ -48,9 +46,6 @@ class ProcessStep(NameSlugModel): ) history = HistoricalRecords() - # Note: approver requirements are defined via StepApproverRequirement through model - # See StepApproverRequirement below - class Meta: verbose_name = "مرحله فرآیند" verbose_name_plural = "مراحل فرآیند" @@ -68,7 +63,6 @@ class ProcessStep(NameSlugModel): """دریافت مراحلی که به این مرحله وابسته هستند""" return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True) - class StepDependency(models.Model): """مدل وابستگی بین مراحل""" dependent_step = models.ForeignKey( @@ -296,7 +290,6 @@ class ProcessInstance(SluggedModel): return False return True - class StepInstance(models.Model): """مدل نمونه مرحله (برای هر مرحله در هر درخواست)""" process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند") @@ -360,27 +353,6 @@ class StepInstance(models.Model): """دریافت آخرین رد شدن""" return self.rejections.order_by('-created_at').first() - # -------- Multi-role approval helpers -------- - def required_roles(self): - return [req.role for req in self.step.approver_requirements.select_related('role').all()] - - def approvals_by_role(self): - decisions = {} - for a in self.approvals.select_related('role').order_by('created_at'): - decisions[a.role_id] = a.decision - return decisions - - def is_fully_approved(self) -> bool: - req_roles = self.required_roles() - if not req_roles: - return True - role_to_decision = self.approvals_by_role() - for r in req_roles: - if role_to_decision.get(r.id) != 'approved': - return False - return True - - class StepRejection(models.Model): """مدل رد شدن مرحله""" step_instance = models.ForeignKey( @@ -418,35 +390,37 @@ class StepRejection(models.Model): self.step_instance.save() super().save(*args, **kwargs) - -class StepApproverRequirement(models.Model): - """Required approver roles for a step.""" - step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله") - role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش تاییدکننده") - required_count = models.PositiveIntegerField(default=1, verbose_name="تعداد موردنیاز") +class StepRevision(models.Model): + """مدل بازبینی و اصلاح مرحله""" + step_instance = models.ForeignKey( + StepInstance, + on_delete=models.CASCADE, + related_name='revisions', + verbose_name="نمونه مرحله" + ) + rejection = models.ForeignKey( + StepRejection, + on_delete=models.CASCADE, + related_name='revisions', + verbose_name="رد شدن مربوطه" + ) + revised_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + verbose_name="اصلاح کننده", + related_name='step_revisions' + ) + changes_description = models.TextField( + verbose_name="توضیح تغییرات", + help_text="توضیح تغییراتی که برای اصلاح انجام شده" + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ اصلاح") + history = HistoricalRecords() class Meta: - unique_together = ('step', 'role') - verbose_name = "نیازمندی تایید نقش" - verbose_name_plural = "نیازمندی‌های تایید نقش" + verbose_name = "بازبینی مرحله" + verbose_name_plural = "بازبینی‌های مراحل" + ordering = ['-created_at'] def __str__(self): - return f"{self.step} ← {self.role} (x{self.required_count})" - - -class StepApproval(models.Model): - """Approvals per role for a concrete step instance.""" - step_instance = models.ForeignKey(StepInstance, on_delete=models.CASCADE, related_name='approvals', verbose_name="نمونه مرحله") - role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش") - approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="تاییدکننده") - decision = models.CharField(max_length=8, choices=[('approved', 'تایید'), ('rejected', 'رد')], verbose_name='نتیجه') - reason = models.TextField(blank=True, verbose_name='علت (برای رد)') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ') - - class Meta: - unique_together = ('step_instance', 'role') - verbose_name = 'تایید مرحله' - verbose_name_plural = 'تاییدهای مرحله' - - def __str__(self): - return f"{self.step_instance} - {self.role} - {self.decision}" + return f"بازبینی {self.step_instance} توسط {self.revised_by.get_full_name()}" diff --git a/processes/templates/processes/includes/stepper_header.html b/processes/templates/processes/includes/stepper_header.html index 505ecd8..bd30927 100644 --- a/processes/templates/processes/includes/stepper_header.html +++ b/processes/templates/processes/includes/stepper_header.html @@ -6,7 +6,6 @@
{% endif %} - {{ forloop.counter }} + {{ forloop.counter }} - {{ step.name }} + {{ step.name }} {{ step.description|default:' ' }} diff --git a/processes/templates/processes/instance_summary.html b/processes/templates/processes/instance_summary.html deleted file mode 100644 index cb5171e..0000000 --- a/processes/templates/processes/instance_summary.html +++ /dev/null @@ -1,168 +0,0 @@ - {% extends '_base.html' %} -{% load static %} -{% load humanize %} -{% load common_tags %} - -{% block sidebar %} - {% include 'sidebars/admin.html' %} -{% endblock sidebar %} - -{% block navbar %} - {% include 'navbars/admin.html' %} -{% endblock navbar %} - -{% block title %}گزارش نهایی - درخواست {{ instance.code }}{% endblock %} - -{% block content %} -{% include '_toasts.html' %} -
-
-
-
-
-

گزارش نهایی درخواست {{ instance.code }}

- - اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} - -
-
- {% if invoice %} - پرینت فاکتور - {% endif %} - پرینت گواهی - بازگشت -
-
- -
-
-
-
-
فاکتور نهایی
-
-
- {% if invoice %} -
-
مبلغ نهایی
{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان
-
پرداختی‌ها
{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان
-
مانده
{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان
-
-
- - - - - - - - - - - {% for it in rows %} - - - - - - - {% empty %} - - {% endfor %} - -
آیتمتعدادقیمت واحدقیمت کل
{{ it.item.name }}{{ it.quantity }}{{ it.unit_price|floatformat:0|intcomma:False }}{{ it.total_price|floatformat:0|intcomma:False }}
اطلاعاتی ندارد
-
- {% else %} -
فاکتور نهایی ثبت نشده است.
- {% endif %} -
-
-
- -
-
-
-
گزارش نصب
- {% if latest_report and latest_report.assignment and latest_report.assignment.installer %} - نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }} - {% endif %} -
-
- {% if latest_report %} -
-
-

تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}

-

سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}

-

شماره پلمپ: {{ latest_report.seal_number|default:'-' }}

-
-
-

کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}

-

UTM X: {{ latest_report.utm_x|default:'-' }}

-

UTM Y: {{ latest_report.utm_y|default:'-' }}

-
-
- {% if latest_report.description %} -
-

توضیحات:

-
{{ latest_report.description }}
-
- {% endif %} -
-
عکس‌ها
-
- {% for p in latest_report.photos.all %} -
photo
- {% empty %} -
بدون عکس
- {% endfor %} -
- {% else %} -
گزارش نصب ثبت نشده است.
- {% endif %} -
-
-
- -
-
-
-
تراکنش‌ها
-
-
-
- - - - - - - - - - - - {% for p in payments %} - - - - - - - - {% empty %} - - {% endfor %} - -
نوعمبلغتاریخروششماره مرجع/چک
{% if p.direction == 'in' %}دریافتی{% else %}پرداختی{% endif %}{{ p.amount|floatformat:0|intcomma:False }} تومان{{ p.payment_date|date:'Y/m/d' }}{{ p.get_payment_method_display }}{{ p.reference_number|default:'-' }}
بدون تراکنش
-
-
-
-
- -
-
-
-
-{% endblock %} - - diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index 5a0331f..acf274c 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -28,113 +28,17 @@
-
-
-
لیست درخواست‌ها
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- کل درخواست‌ها -
-

{{ total_count }}

-
-
-
- - - -
-
-
-
-
-
-
-
-
-
- تکمیل‌شده -
-

{{ completed_count }}

-
-
-
- - - -
-
-
-
-
-
-
-
-
-
- در حال انجام -
-

{{ in_progress_count }}

-
-
-
- - - -
-
-
-
-
-
-
-
-
-
- در انتظار -
-

{{ pending_count }}

-
-
-
- - - -
-
-
-
-
+
+

درخواست‌ها

+
-
- +
+
@@ -142,8 +46,8 @@ - - + + @@ -157,9 +61,9 @@ - - - + + +
شناسهمرحله فعلی شماره اشتراک آب نمایندهاستاناموردرخواست‌کنندهاولویت وضعیت تاریخ ایجاد عملیات{{ inst.current_step.name|default:"--" }} {{ inst.well.water_subscription_number }} {% if inst.representative %}{{ inst.representative.get_full_name }}{% else %}-{% endif %}{% if inst.well and inst.well.county %}{{ inst.well.county }}{% else %}-{% endif %}{% if inst.well and inst.well.affairs %}{{ inst.well.affairs }}{% else %}-{% endif %}{{ inst.get_status_display_with_color|safe }}{% if inst.requester %}{{ inst.requester.get_full_name }}{% else %}-{% endif %}{{ inst.get_priority_display }}{{ inst.get_status_display }} {{ inst.jcreated }}
@@ -168,15 +72,9 @@