Compare commits
No commits in common. "bf4047714cef9455118dd0a5b96220f011b71d60" and "0201779fb5637c3ca00631795a9d48ed48672383" have entirely different histories.
bf4047714c
...
0201779fb5
49 changed files with 1075 additions and 30953 deletions
|
@ -167,7 +167,7 @@ JAZZMIN_SETTINGS = {
|
||||||
# Copyright on the footer
|
# Copyright on the footer
|
||||||
"copyright": "سامانه شفافیت",
|
"copyright": "سامانه شفافیت",
|
||||||
# Logo to use for your site, must be present in static files, used for brand on top left
|
# 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)
|
# Relative paths to custom CSS/JS scripts (must be present in static files)
|
||||||
"custom_css": "../static/admin/css/custom_rtl.css",
|
"custom_css": "../static/admin/css/custom_rtl.css",
|
||||||
"custom_js": None,
|
"custom_js": None,
|
||||||
|
|
|
@ -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.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
@ -17,27 +17,6 @@ class Migration(migrations.Migration):
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name='HistoricalProfile',
|
name='HistoricalProfile',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -51,7 +30,6 @@ class Migration(migrations.Migration):
|
||||||
('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
|
('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='شماره کارت')),
|
('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='شماره حساب')),
|
('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_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='شماره تماس ۲')),
|
('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='تصویر')),
|
('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='آدرس')),
|
('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='شماره کارت')),
|
('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='شماره حساب')),
|
('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_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='شماره تماس ۲')),
|
('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='تصویر')),
|
('pic', models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
|
||||||
|
|
34
accounts/migrations/0002_company.py
Normal file
34
accounts/migrations/0002_company.py
Normal file
|
@ -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ها',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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='نام بانک'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -64,18 +64,21 @@ layout-wide customizer-hide
|
||||||
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
|
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- /Logo -->
|
||||||
|
<h4 class="mb-2">Welcome to Sneat! 👋</h4>
|
||||||
|
<p class="mb-4">Please sign-in to your account and start the adventure</p>
|
||||||
|
|
||||||
<form id="formAuthentication" class="mb-3 fv-plugins-bootstrap5 fv-plugins-framework" method="post" novalidate="novalidate">
|
<form id="formAuthentication" class="mb-3 fv-plugins-bootstrap5 fv-plugins-framework" method="post" novalidate="novalidate">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3 fv-plugins-icon-container">
|
<div class="mb-3 fv-plugins-icon-container">
|
||||||
<label for="email" class="form-label">نام کاربری</label>
|
<label for="email" class="form-label">Email or Username</label>
|
||||||
<input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
|
<input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
|
||||||
<div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
|
<div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
|
||||||
<div class="mb-3 form-password-toggle fv-plugins-icon-container">
|
<div class="mb-3 form-password-toggle fv-plugins-icon-container">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<label class="form-label" for="password">رمز عبور</label>
|
<label class="form-label" for="password">Password</label>
|
||||||
<a href="auth-forgot-password-basic.html">
|
<a href="auth-forgot-password-basic.html">
|
||||||
<small>رمز عبور را فراموش کرده اید؟</small>
|
<small>Forgot Password?</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group input-group-merge has-validation">
|
<div class="input-group input-group-merge has-validation">
|
||||||
|
@ -83,12 +86,43 @@ layout-wide customizer-hide
|
||||||
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
|
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
|
||||||
</div><div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div>
|
</div><div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button class="btn btn-primary d-grid w-100" type="submit">ورود</button>
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="remember-me">
|
||||||
|
<label class="form-check-label" for="remember-me">
|
||||||
|
Remember Me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-primary d-grid w-100" type="submit">Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden"></form>
|
<input type="hidden"></form>
|
||||||
|
|
||||||
|
<p class="text-center">
|
||||||
|
<span>New on our platform?</span>
|
||||||
|
<a href="auth-register-basic.html">
|
||||||
|
<span>Create an account</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="divider my-4">
|
||||||
|
<div class="divider-text">or</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<a href="javascript:;" class="btn btn-icon btn-label-facebook me-3">
|
||||||
|
<i class="tf-icons bx bxl-facebook"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="javascript:;" class="btn btn-icon btn-label-google-plus me-3">
|
||||||
|
<i class="tf-icons bx bxl-google-plus"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="javascript:;" class="btn btn-icon btn-label-twitter">
|
||||||
|
<i class="tf-icons bx bxl-twitter"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /Register -->
|
<!-- /Register -->
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from accounts.views import login_view, dashboard, customer_list, add_customer_ajax, edit_customer_ajax, get_customer_data, logout_view
|
from accounts.views import login_view, dashboard, customer_list, add_customer_ajax, edit_customer_ajax, get_customer_data
|
||||||
|
|
||||||
app_name = "accounts"
|
app_name = "accounts"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('login/', login_view, name='login'),
|
path('login/', login_view, name='login'),
|
||||||
path('logout/', logout_view, name='logout'),
|
|
||||||
path('dashboard/', dashboard, name='dashboard'),
|
path('dashboard/', dashboard, name='dashboard'),
|
||||||
path('customers/', customer_list, name='customer_list'),
|
path('customers/', customer_list, name='customer_list'),
|
||||||
path('customers/add/', add_customer_ajax, name='add_customer_ajax'),
|
path('customers/add/', add_customer_ajax, name='add_customer_ajax'),
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login, authenticate, logout
|
from django.contrib.auth import login, authenticate
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_POST, require_GET
|
from django.views.decorators.http import require_POST, require_GET
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from accounts.models import Profile
|
from accounts.models import Profile
|
||||||
from accounts.forms import CustomerForm
|
from accounts.forms import CustomerForm
|
||||||
from common.consts import UserRoles
|
from common.consts import UserRoles
|
||||||
|
@ -21,12 +21,15 @@ def login_view(request):
|
||||||
username = request.POST.get("username")
|
username = request.POST.get("username")
|
||||||
password = request.POST.get("password")
|
password = request.POST.get("password")
|
||||||
user = authenticate(request, username=username, password=password)
|
user = authenticate(request, username=username, password=password)
|
||||||
if user is not None:
|
# if user is not None:
|
||||||
login(request, user)
|
# login(request, user)
|
||||||
return redirect("processes:request_list")
|
# if user.profile.has_none_of([UserRoles.MANAGER]):
|
||||||
else:
|
# return redirect("dashboard:dashboard")
|
||||||
messages.error(request, "کاربری با این مشخصات یافت نشد!")
|
# else:
|
||||||
return redirect("accounts:login")
|
# return redirect("dashboard:admin_dashboard")
|
||||||
|
# else:
|
||||||
|
# messages.error(request, "کاربری با این مشخصات یافت نشد!")
|
||||||
|
# return redirect("accounts:login")
|
||||||
|
|
||||||
return render(request, "accounts/login.html")
|
return render(request, "accounts/login.html")
|
||||||
|
|
||||||
|
@ -34,7 +37,7 @@ def dashboard(request):
|
||||||
return render(request, "accounts/dashboard.html")
|
return render(request, "accounts/dashboard.html")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def customer_list(request):
|
def customer_list(request):
|
||||||
# Get all profiles that have customer role
|
# Get all profiles that have customer role
|
||||||
customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
|
customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
|
||||||
|
@ -160,9 +163,3 @@ def get_customer_data(request, customer_id):
|
||||||
},
|
},
|
||||||
'form_html': form_html
|
'form_html': form_html
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def logout_view(request):
|
|
||||||
"""Log out current user and redirect to login page."""
|
|
||||||
logout(request)
|
|
||||||
return redirect("accounts:login")
|
|
||||||
|
|
|
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -9,7 +9,6 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0001_initial'),
|
|
||||||
('processes', '0001_initial'),
|
('processes', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -24,8 +23,10 @@ class Migration(migrations.Migration):
|
||||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||||
('title', models.CharField(max_length=200, verbose_name='عنوان')),
|
('title', models.CharField(max_length=200, verbose_name='عنوان')),
|
||||||
('body', models.TextField(verbose_name='متن قالب (با جایگزین\u200cها)')),
|
('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='فعال')),
|
('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={
|
options={
|
||||||
'verbose_name': 'قالب گواهی',
|
'verbose_name': 'قالب گواهی',
|
||||||
|
|
|
@ -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='شرکت صادر کننده'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,7 +2,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load processes_tags %}
|
{% load processes_tags %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load accounts_tags %}
|
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include 'sidebars/admin.html' %}
|
{% include 'sidebars/admin.html' %}
|
||||||
|
@ -80,11 +79,7 @@
|
||||||
{% else %}<span></span>{% endif %}
|
{% else %}<span></span>{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if request.user|is_broker %}
|
|
||||||
<button class="btn btn-primary" type="submit">تایید و پایان</button>
|
<button class="btn btn-primary" type="submit">تایید و پایان</button>
|
||||||
{% else %}
|
|
||||||
<button class="btn btn-primary" type="button" disabled>تایید و پایان</button>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,6 @@ from processes.models import ProcessInstance, StepInstance
|
||||||
from invoices.models import Invoice
|
from invoices.models import Invoice
|
||||||
from installations.models import InstallationReport
|
from installations.models import InstallationReport
|
||||||
from .models import CertificateTemplate, CertificateInstance
|
from .models import CertificateTemplate, CertificateInstance
|
||||||
from common.consts import UserRoles
|
|
||||||
|
|
||||||
from _helpers.jalali import Gregorian
|
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
|
next_step = instance.process.steps.filter(order__gt=instance.current_step.order).first() if instance.current_step else None
|
||||||
|
|
||||||
if request.method == 'POST':
|
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 = True
|
||||||
cert.approved_at = timezone.now()
|
cert.approved_at = timezone.now()
|
||||||
cert.save()
|
cert.save()
|
||||||
|
@ -98,10 +89,7 @@ def certificate_step(request, instance_id, step_id):
|
||||||
instance.current_step = next_step
|
instance.current_step = next_step
|
||||||
instance.save()
|
instance.save()
|
||||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
|
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
|
return redirect('processes:request_list')
|
||||||
instance.status = 'completed'
|
|
||||||
instance.save()
|
|
||||||
return redirect('processes:instance_summary', instance_id=instance.id)
|
|
||||||
|
|
||||||
return render(request, 'certificates/step.html', {
|
return render(request, 'certificates/step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
|
|
@ -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 django.db.models.deletion
|
||||||
|
import simple_history.models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -10,7 +11,6 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0001_initial'),
|
|
||||||
('processes', '0001_initial'),
|
('processes', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
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='اسلاگ')),
|
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
|
||||||
('name', models.CharField(max_length=100, verbose_name='نام')),
|
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||||
('body', models.TextField(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={
|
options={
|
||||||
'verbose_name': 'قالب قرارداد',
|
'verbose_name': 'قالب قرارداد',
|
||||||
|
@ -57,4 +58,61 @@ class Migration(migrations.Migration):
|
||||||
'ordering': ['-created'],
|
'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),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -41,7 +41,6 @@
|
||||||
<div class="bs-stepper-content">
|
<div class="bs-stepper-content">
|
||||||
<div class="card border">
|
<div class="card border">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if can_view_contract_body %}
|
|
||||||
{% if template.company.logo %}
|
{% if template.company.logo %}
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<img src="{{ template.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
|
<img src="{{ template.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
|
||||||
|
@ -68,9 +67,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-warning mb-0">شما دسترسی به مشاهده متن قرارداد را ندارید.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" class="d-flex justify-content-between mt-3">
|
<form method="post" class="d-flex justify-content-between mt-3">
|
||||||
|
@ -81,17 +77,9 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
{% if is_broker %}
|
<button type="submit" class="btn btn-primary">بعدی</button>
|
||||||
<button type="submit" class="btn btn-primary">تایید و بعدی</button>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
<button class="btn btn-success" type="button">اتمام</button>
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% if is_broker %}
|
|
||||||
<button class="btn btn-success" type="submit">اتمام</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="btn btn-success" type="button" disabled>اتمام</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,6 @@ from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
from processes.models import ProcessInstance, StepInstance
|
from processes.models import ProcessInstance, StepInstance
|
||||||
from common.consts import UserRoles
|
|
||||||
from .models import ContractTemplate, ContractInstance
|
from .models import ContractTemplate, ContractInstance
|
||||||
from _helpers.utils import jalali_converter2
|
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)
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||||
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
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()
|
template_obj = ContractTemplate.objects.first()
|
||||||
if not template_obj:
|
if not template_obj:
|
||||||
return render(request, 'contracts/contract_missing.html', {'instance': instance})
|
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.rendered_body = rendered
|
||||||
contract.save()
|
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 request.method == 'POST':
|
||||||
if not is_broker:
|
|
||||||
from django.http import JsonResponse
|
|
||||||
return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
|
|
||||||
StepInstance.objects.update_or_create(
|
StepInstance.objects.update_or_create(
|
||||||
process_instance=instance,
|
process_instance=instance,
|
||||||
step=step,
|
step=step,
|
||||||
|
@ -92,8 +74,6 @@ def contract_step(request, instance_id, step_id):
|
||||||
'template': template_obj,
|
'template': template_obj,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_step,
|
'next_step': next_step,
|
||||||
'is_broker': is_broker,
|
|
||||||
'can_view_contract_body': can_view_contract_body,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('invoices', '0001_initial'),
|
('invoices', '0002_historicalpayment_receipt_image_and_more'),
|
||||||
('processes', '0001_initial'),
|
('processes', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
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_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')),
|
('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='توضیحات')),
|
('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='اختصاص')),
|
('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='ایجادکننده')),
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='ایجادکننده')),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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='تاریخ تایید'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load processes_tags %}
|
{% load processes_tags %}
|
||||||
{% load common_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
@ -42,15 +41,12 @@
|
||||||
|
|
||||||
<div class="bs-stepper-content">
|
<div class="bs-stepper-content">
|
||||||
|
|
||||||
{% if show_denied_msg %}
|
|
||||||
<div class="alert alert-warning mb-3">شما اجازه تعیین نصاب را ندارید.</div>
|
|
||||||
{% endif %}
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">نصاب</label>
|
<label class="form-label">نصاب</label>
|
||||||
<select name="installer_id" class="form-select" {% if read_only %}disabled{% endif %} required>
|
<select name="installer_id" class="form-select" required>
|
||||||
<option value="">انتخاب کنید...</option>
|
<option value="">انتخاب کنید...</option>
|
||||||
{% for p in installers %}
|
{% for p in installers %}
|
||||||
<option value="{{ p.user.id }}" {% if assignment.installer and p.user.id == assignment.installer.id %}selected{% endif %}>{{ p.user.get_full_name }} ({{ p.user.username }})</option>
|
<option value="{{ p.user.id }}" {% if assignment.installer and p.user.id == assignment.installer.id %}selected{% endif %}>{{ p.user.get_full_name }} ({{ p.user.username }})</option>
|
||||||
|
@ -59,39 +55,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">تاریخ مراجعه نصاب</label>
|
<label class="form-label">تاریخ مراجعه نصاب</label>
|
||||||
<input type="text" id="id_scheduled_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if read_only %}disabled{% endif %} readonly required value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y/m/d' }}{% endif %}">
|
<input type="text" id="id_scheduled_date_display" class="form-control" placeholder="انتخاب تاریخ" readonly required value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y/m/d' }}{% endif %}">
|
||||||
<input type="hidden" id="id_scheduled_date" name="scheduled_date" value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y-m-d' }}{% endif %}">
|
<input type="hidden" id="id_scheduled_date" name="scheduled_date" value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y-m-d' }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if assignment.assigned_by or assignment.installer %}
|
|
||||||
<div class="mt-3 border rounded p-3 bg-light">
|
|
||||||
<div class="row g-2">
|
|
||||||
{% if assignment.assigned_by %}
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<div class="small text-muted">تعیینکننده نصاب</div>
|
|
||||||
<div>{{ assignment.assigned_by.get_full_name|default:assignment.assigned_by.username }} <span class="text-muted">({{ assignment.assigned_by.username }})</span></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if assignment.updated %}
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<div class="small text-muted">تاریخ ثبت/ویرایش</div>
|
|
||||||
<div>{{ assignment.updated|to_jalali }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
{% if previous_step %}
|
{% if previous_step %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
|
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_manager %}
|
|
||||||
<button class="btn btn-primary" type="submit">ثبت و ادامه</button>
|
<button class="btn btn-primary" type="submit">ثبت و ادامه</button>
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load processes_tags %}
|
{% load processes_tags %}
|
||||||
{% load common_tags %}
|
{% load common_tags %}
|
||||||
{% load accounts_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
@ -20,18 +19,6 @@
|
||||||
|
|
||||||
<!-- Persian Date Picker CSS -->
|
<!-- Persian Date Picker CSS -->
|
||||||
<link rel="stylesheet" href="https://unpkg.com/persian-datepicker@latest/dist/css/persian-datepicker.min.css">
|
<link rel="stylesheet" href="https://unpkg.com/persian-datepicker@latest/dist/css/persian-datepicker.min.css">
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Red styling for removal checkboxes when checked */
|
|
||||||
.removal-checkbox:checked {
|
|
||||||
background-color: #dc3545 !important;
|
|
||||||
border-color: #dc3545 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.removal-checkbox:checked:focus {
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -54,27 +41,13 @@
|
||||||
{% stepper_header instance step %}
|
{% stepper_header instance step %}
|
||||||
|
|
||||||
<div class="bs-stepper-content">
|
<div class="bs-stepper-content">
|
||||||
|
|
||||||
{% if report and not edit_mode %}
|
{% if report and not edit_mode %}
|
||||||
<div class="card mb-3 border">
|
<div class="card mb-3 border">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-end">
|
||||||
<div class="d-flex gap-2">
|
|
||||||
{% if request.user|is_installer %}
|
|
||||||
<a href="?edit=1" class="btn btn-primary">ویرایش گزارش نصب</a>
|
<a href="?edit=1" class="btn btn-primary">ویرایش گزارش نصب</a>
|
||||||
{% else %}
|
|
||||||
<button type="button" class="btn btn-primary" disabled>ویرایش گزارش نصب</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %}
|
|
||||||
<div class="alert alert-danger d-flex align-items-start" role="alert">
|
|
||||||
<i class="bx bx-error-circle me-2"></i>
|
|
||||||
<div>
|
|
||||||
<div><strong>این گزارش رد شده است.</strong></div>
|
|
||||||
<div class="mt-1 small">علت رد: {{ step_instance.get_latest_rejection.reason }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
|
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
|
||||||
|
@ -94,9 +67,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if request.user|is_manager or request.user|is_admin %}
|
|
||||||
<hr>
|
|
||||||
{% endif %}
|
|
||||||
<h6>عکسها</h6>
|
<h6>عکسها</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for p in report.photos.all %}
|
{% for p in report.photos.all %}
|
||||||
|
@ -145,42 +115,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if approver_statuses %}
|
|
||||||
<div class="card border mt-2">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">وضعیت تاییدها</h6>
|
|
||||||
{% if user_can_approve %}
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal" {% if step_instance and step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body py-3">
|
|
||||||
<div class="row g-2">
|
|
||||||
{% for st in approver_statuses %}
|
|
||||||
<div class="col-12 col-md-6 col-lg-4">
|
|
||||||
<div class="d-flex flex-column border rounded px-2 py-1">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
|
|
||||||
{% if st.status == 'approved' %}
|
|
||||||
<span class="badge bg-success">تایید شد</span>
|
|
||||||
{% elif st.status == 'rejected' %}
|
|
||||||
<span class="badge bg-danger">رد شد</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning text-dark">در انتظار</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if st.status == 'rejected' and st.reason %}
|
|
||||||
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<!-- Persistent nav in edit mode (outside cards) -->
|
<!-- Persistent nav in edit mode (outside cards) -->
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
{% if previous_step %}
|
{% if previous_step %}
|
||||||
|
@ -193,9 +127,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if not request.user|is_installer %}
|
|
||||||
<div class="alert alert-warning">شما مجوز ثبت/ویرایش گزارش نصب را ندارید. اطلاعات به صورت فقط خواندنی نمایش داده میشود.</div>
|
|
||||||
{% endif %}
|
|
||||||
<form method="post" enctype="multipart/form-data" id="installation-report-form">
|
<form method="post" enctype="multipart/form-data" id="installation-report-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -203,42 +134,40 @@
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">تاریخ مراجعه</label>
|
<label class="form-label">تاریخ مراجعه</label>
|
||||||
<input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if not request.user|is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
|
<input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
|
||||||
<input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% endif %}">
|
<input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">سریال کنتور جدید</label>
|
<label class="form-label">سریال کنتور جدید</label>
|
||||||
<input type="text" class="form-control" name="new_water_meter_serial" value="{% if report and edit_mode %}{{ report.new_water_meter_serial|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
|
<input type="text" class="form-control" name="new_water_meter_serial">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">شماره پلمپ</label>
|
<label class="form-label">شماره پلمپ</label>
|
||||||
<input type="text" class="form-control" name="seal_number" value="{% if report and edit_mode %}{{ report.seal_number|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
|
<input type="text" class="form-control" name="seal_number">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 d-flex align-items-end">
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious" {% if not request.user|is_installer %}disabled{% endif %} {% if report and edit_mode and report.is_meter_suspicious %}checked{% endif %}>
|
<input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious">
|
||||||
<label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
|
<label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">UTM X</label>
|
<label class="form-label">UTM X</label>
|
||||||
<input type="number" step="0.000001" class="form-control" name="utm_x" value="{% if report and edit_mode and report.utm_x %}{{ report.utm_x }}{% elif instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
|
<input type="number" step="0.000001" class="form-control" name="utm_x" value="{% if instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">UTM Y</label>
|
<label class="form-label">UTM Y</label>
|
||||||
<input type="number" step="0.000001" class="form-control" name="utm_y" value="{% if report and edit_mode and report.utm_y %}{{ report.utm_y }}{% elif instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
|
<input type="number" step="0.000001" class="form-control" name="utm_y" value="{% if instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">توضیحات (اختیاری)</label>
|
<label class="form-label">توضیحات (اختیاری)</label>
|
||||||
<textarea class="form-control" rows="3" name="description" {% if not request.user|is_installer %}readonly{% endif %}>{% if report and edit_mode %}{{ report.description|default_if_none:'' }}{% endif %}</textarea>
|
<textarea class="form-control" rows="3" name="description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<label class="form-label mb-0">عکسها</label>
|
<label class="form-label mb-0">عکسها</label>
|
||||||
{% if request.user|is_installer %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="btnAddPhoto"><i class="bx bx-plus"></i> افزودن عکس</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" id="btnAddPhoto"><i class="bx bx-plus"></i> افزودن عکس</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if report %}
|
{% if report %}
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
|
@ -246,9 +175,7 @@
|
||||||
<div class="col-6 col-md-3 mb-2" id="existing-photo-{{ p.id }}">
|
<div class="col-6 col-md-3 mb-2" id="existing-photo-{{ p.id }}">
|
||||||
<div class="position-relative border rounded p-1">
|
<div class="position-relative border rounded p-1">
|
||||||
<img class="img-fluid rounded" src="{{ p.image.url }}" alt="photo">
|
<img class="img-fluid rounded" src="{{ p.image.url }}" alt="photo">
|
||||||
{% if request.user|is_installer %}
|
<button type="button" class="btn btn-sm btn-danger position-absolute" style="top:6px; left:6px;" onclick="markDeletePhoto({{ p.id }})" title="حذف/برگردان"><i class='bx bx-trash'></i></button>
|
||||||
<button type="button" class="btn btn-sm btn-danger position-absolute" style="top:6px; left:6px;" onclick="markDeletePhoto('{{ p.id }}')" title="حذف/برگردان"><i class="bx bx-trash"></i></button>
|
|
||||||
{% endif %}
|
|
||||||
<input type="hidden" name="del_photo_{{ p.id }}" id="del-photo-{{ p.id }}" value="0">
|
<input type="hidden" name="del_photo_{{ p.id }}" id="del-photo-{{ p.id }}" value="0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -284,7 +211,7 @@
|
||||||
{% for qi in quote_items %}
|
{% for qi in quote_items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="form-check-input removal-checkbox" name="rem_{{ qi.item.id }}_type" value="remove" title="حذف در نصب" {% if removed_qty|get_item:qi.item.id %}checked{% endif %}>
|
<input type="checkbox" class="form-check-input" name="rem_{{ qi.item.id }}_type" value="remove" title="حذف در نصب" {% if removed_qty|get_item:qi.item.id %}checked{% endif %}>
|
||||||
<input type="hidden" name="rem_{{ qi.item.id }}_qty" value="{% if removed_qty|get_item:qi.item.id %}{{ removed_qty|get_item:qi.item.id }}{% else %}{{ qi.quantity }}{% endif %}">
|
<input type="hidden" name="rem_{{ qi.item.id }}_qty" value="{% if removed_qty|get_item:qi.item.id %}{{ removed_qty|get_item:qi.item.id }}{% else %}{{ qi.quantity }}{% endif %}">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -358,11 +285,7 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if request.user|is_installer %}
|
|
||||||
<button type="submit" class="btn btn-primary" form="installation-report-form">ثبت گزارش</button>
|
<button type="submit" class="btn btn-primary" form="installation-report-form">ثبت گزارش</button>
|
||||||
{% else %}
|
|
||||||
<button type="button" class="btn btn-primary" disabled>ثبت گزارش</button>
|
|
||||||
{% endif %}
|
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-success">بعدی</a>
|
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-success">بعدی</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -375,58 +298,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Approve Modal -->
|
|
||||||
<div class="modal fade" id="approveModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="approve">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">تایید گزارش نصب</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
آیا از تایید این گزارش اطمینان دارید؟
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-success">تایید</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reject Modal -->
|
|
||||||
<div class="modal fade" id="rejectModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="reject">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">رد گزارش نصب</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">علت رد</label>
|
|
||||||
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-danger">ثبت رد</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
@ -574,3 +445,4 @@
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,7 @@ from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from accounts.models import Profile
|
from accounts.models import Profile
|
||||||
from common.consts import UserRoles
|
from common.consts import UserRoles
|
||||||
from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval
|
from processes.models import ProcessInstance, StepInstance
|
||||||
from accounts.models import Role
|
|
||||||
from invoices.models import Item, Quote, QuoteItem
|
from invoices.models import Item, Quote, QuoteItem
|
||||||
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
|
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
|
||||||
from decimal import Decimal, InvalidOperation
|
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()
|
installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value).select_related('user').all()
|
||||||
assignment, _ = InstallationAssignment.objects.get_or_create(process_instance=instance)
|
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 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')
|
installer_id = request.POST.get('installer_id')
|
||||||
scheduled_date = (request.POST.get('scheduled_date') or '').strip()
|
scheduled_date = (request.POST.get('scheduled_date') or '').strip()
|
||||||
assignment.installer_id = installer_id or None
|
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:step_detail', instance_id=instance.id, step_id=next_step.id)
|
||||||
return redirect('processes:request_list')
|
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', {
|
return render(request, 'installations/installation_assign_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
'step': step,
|
'step': step,
|
||||||
|
@ -66,9 +50,6 @@ def installation_assign_step(request, instance_id, step_id):
|
||||||
'installers': installers,
|
'installers': installers,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_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()
|
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
||||||
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
|
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
|
||||||
existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first()
|
existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first()
|
||||||
# Only installers can enter edit mode
|
edit_mode = True if request.GET.get('edit') == '1' else False
|
||||||
user_is_installer = hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.INSTALLER)
|
print("edit_mode", edit_mode)
|
||||||
edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False
|
|
||||||
# current quote items baseline
|
# current quote items baseline
|
||||||
quote = Quote.objects.filter(process_instance=instance).first()
|
quote = Quote.objects.filter(process_instance=instance).first()
|
||||||
quote_items = list(quote.items.select_related('item').all()) if quote else []
|
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}
|
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')
|
items = Item.objects.all().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)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
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()
|
description = (request.POST.get('description') or '').strip()
|
||||||
visited_date = (request.POST.get('visited_date') or '').strip()
|
visited_date = (request.POST.get('visited_date') or '').strip()
|
||||||
if '/' in visited_date:
|
if '/' in visited_date:
|
||||||
|
@ -232,7 +134,6 @@ def installation_report_step(request, instance_id, step_id):
|
||||||
report.is_meter_suspicious = is_suspicious
|
report.is_meter_suspicious = is_suspicious
|
||||||
report.utm_x = utm_x
|
report.utm_x = utm_x
|
||||||
report.utm_y = utm_y
|
report.utm_y = utm_y
|
||||||
report.approved = False # back to awaiting approval after edits
|
|
||||||
report.save()
|
report.save()
|
||||||
# delete selected existing photos
|
# delete selected existing photos
|
||||||
for key, val in request.POST.items():
|
for key, val in request.POST.items():
|
||||||
|
@ -310,17 +211,18 @@ def installation_report_step(request, instance_id, step_id):
|
||||||
total_price=total,
|
total_price=total,
|
||||||
)
|
)
|
||||||
|
|
||||||
# After installer submits/edits, set step back to in_progress and clear approvals
|
# complete step
|
||||||
step_instance.status = 'in_progress'
|
StepInstance.objects.update_or_create(
|
||||||
step_instance.completed_at = None
|
process_instance=instance,
|
||||||
step_instance.save()
|
step=step,
|
||||||
try:
|
defaults={'status': 'completed', 'completed_at': timezone.now()}
|
||||||
step_instance.approvals.all().delete()
|
)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.')
|
if next_step:
|
||||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
|
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
|
# Build prefill maps from existing report changes
|
||||||
removed_ids = set()
|
removed_ids = set()
|
||||||
|
@ -348,9 +250,6 @@ def installation_report_step(request, instance_id, step_id):
|
||||||
'added_map': added_map,
|
'added_map': added_map,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_step,
|
'next_step': next_step,
|
||||||
'step_instance': step_instance,
|
|
||||||
'approver_statuses': approver_statuses,
|
|
||||||
'user_can_approve': user_can_approve,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ from .models import Item, Quote, QuoteItem, Invoice, InvoiceItem, Payment
|
||||||
|
|
||||||
@admin.register(Item)
|
@admin.register(Item)
|
||||||
class ItemAdmin(SimpleHistoryAdmin):
|
class ItemAdmin(SimpleHistoryAdmin):
|
||||||
list_display = ['name', 'unit_price', 'default_quantity', '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_special', 'is_active', 'created_by']
|
list_filter = ['is_default_in_quotes', 'is_active', 'created_by']
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
readonly_fields = ['deleted_at', 'created', 'updated']
|
readonly_fields = ['deleted_at', 'created', 'updated']
|
||||||
|
|
|
@ -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 django.db.models.deletion
|
||||||
import simple_history.models
|
import simple_history.models
|
||||||
|
@ -29,7 +29,6 @@ class Migration(migrations.Migration):
|
||||||
('name', models.CharField(max_length=100, verbose_name='نام')),
|
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||||
('description', models.TextField(blank=True, verbose_name='توضیحات')),
|
('description', models.TextField(blank=True, verbose_name='توضیحات')),
|
||||||
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, 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فرض')),
|
('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فاکتورها')),
|
('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
|
||||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
@ -122,12 +121,10 @@ class Migration(migrations.Migration):
|
||||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||||
('amount', models.DecimalField(decimal_places=2, max_digits=15, 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='روش پرداخت')),
|
('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='تاریخ پرداخت')),
|
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
|
||||||
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
|
('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_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
('history_date', models.DateTimeField(db_index=True)),
|
('history_date', models.DateTimeField(db_index=True)),
|
||||||
('history_change_reason', models.CharField(max_length=100, null=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='نام')),
|
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||||
('description', models.TextField(blank=True, verbose_name='توضیحات')),
|
('description', models.TextField(blank=True, verbose_name='توضیحات')),
|
||||||
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, 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فرض')),
|
('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فاکتورها')),
|
('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='ایجاد کننده')),
|
('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='حذف شده')),
|
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||||
('amount', models.DecimalField(decimal_places=2, max_digits=15, 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='روش پرداخت')),
|
('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='تاریخ پرداخت')),
|
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
|
||||||
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
|
('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='ثبت کننده')),
|
('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='فاکتور')),
|
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice', verbose_name='فاکتور')),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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='تصویر فیش'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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='شماره مرجع'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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='نوع تراکنش'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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='نوع ویژه'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -50,9 +50,7 @@
|
||||||
<div class="card border">
|
<div class="card border">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">فاکتور نهایی</h5>
|
<h5 class="mb-0">فاکتور نهایی</h5>
|
||||||
{% if is_manager %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="openSpecialChargeModal()"><i class="bx bx-plus"></i> افزودن هزینه تعمیر/تعویض</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="openSpecialChargeModal()"><i class="bx bx-plus"></i> افزودن هزینه تعمیر/تعویض</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -129,9 +127,7 @@
|
||||||
<td class="text-end">{{ si.unit_price|floatformat:0|intcomma:False }}</td>
|
<td class="text-end">{{ si.unit_price|floatformat:0|intcomma:False }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
{{ si.total_price|floatformat:0|intcomma:False }}
|
{{ si.total_price|floatformat:0|intcomma:False }}
|
||||||
{% if is_manager %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="deleteSpecial('{{ si.id }}')" title="حذف"><i class="bx bx-trash"></i></button>
|
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="deleteSpecial('{{ si.id }}')" title="حذف"><i class="bx bx-trash"></i></button>
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -168,11 +164,7 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
{% if is_manager %}
|
|
||||||
<button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button>
|
<button type="button" class="btn btn-primary" id="btnApproveFinalInvoice">تایید و ادامه</button>
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load processes_tags %}
|
{% load processes_tags %}
|
||||||
{% load common_tags %}
|
{% load common_tags %}
|
||||||
{% load accounts_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
@ -47,7 +46,6 @@
|
||||||
<div class="bs-stepper-content">
|
<div class="bs-stepper-content">
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
{% if is_broker %}
|
|
||||||
<div class="col-12 col-lg-5">
|
<div class="col-12 col-lg-5">
|
||||||
<div class="card border h-100">
|
<div class="card border h-100">
|
||||||
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
|
<div class="card-header"><h5 class="mb-0">ثبت تراکنش تسویه</h5></div>
|
||||||
|
@ -80,11 +78,11 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">شماره مرجع/چک</label>
|
<label class="form-label">شماره مرجع</label>
|
||||||
<input type="text" class="form-control" name="reference_number" id="id_reference_number" required>
|
<input type="text" class="form-control" name="reference_number" id="id_reference_number" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">تصویر فیش/چک</label>
|
<label class="form-label">تصویر فیش</label>
|
||||||
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
|
@ -94,39 +92,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="col-12 col-lg-7">
|
||||||
<div class="col-12 {% if is_broker %}col-lg-7{% else %}col-lg-12{% endif %}">
|
|
||||||
<div class="card mb-3 border">
|
<div class="card mb-3 border">
|
||||||
<div class="card-header d-flex justify-content-between">
|
<div class="card-header"><h5 class="mb-0">وضعیت فاکتور</h5></div>
|
||||||
<h5 class="mb-0">وضعیت فاکتور</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-6 col-md-4">
|
<div class="col-6">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="border rounded p-3">
|
||||||
<div class="small text-muted">مبلغ نهایی</div>
|
<div class="small text-muted">مبلغ نهایی</div>
|
||||||
<div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
|
<div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-4">
|
<div class="col-6">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="border rounded p-3">
|
||||||
<div class="small text-muted">پرداختیها</div>
|
|
||||||
<div class="h5 mt-1 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 col-md-4">
|
|
||||||
<div class="border rounded p-3 h-100">
|
|
||||||
<div class="small text-muted">مانده</div>
|
<div class="small text-muted">مانده</div>
|
||||||
<div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
|
<div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 d-flex align-items-center">
|
|
||||||
{% if invoice.remaining_amount <= 0 %}
|
|
||||||
<span class="badge bg-success">تسویه کامل</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning text-dark">باقیمانده دارد</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,8 +123,8 @@
|
||||||
<th>مبلغ</th>
|
<th>مبلغ</th>
|
||||||
<th>تاریخ</th>
|
<th>تاریخ</th>
|
||||||
<th>روش</th>
|
<th>روش</th>
|
||||||
<th class="text-nowrap">شماره مرجع/چک</th>
|
<th>شماره مرجع</th>
|
||||||
<th>عملیات</th>
|
<th style="width:150px">عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -150,7 +132,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
|
<td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
|
||||||
<td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
|
<td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
|
||||||
<td>{{ p.payment_date|date:'Y/m/d' }}</td>
|
<td>{{ p.payment_date|to_jalali }}</td>
|
||||||
<td>{{ p.get_payment_method_display }}</td>
|
<td>{{ p.get_payment_method_display }}</td>
|
||||||
<td>{{ p.reference_number|default:'-' }}</td>
|
<td>{{ p.reference_number|default:'-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -160,9 +142,7 @@
|
||||||
<i class="bx bx-show"></i>
|
<i class="bx bx-show"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_broker %}
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFinalPayment({{ p.id }})" title="حذف" aria-label="حذف"><i class="bx bx-trash"></i></button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف"><i class="bx bx-trash"></i></button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -172,141 +152,20 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer d-flex justify-content-between">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if approver_statuses %}
|
|
||||||
<div class="card border mt-2">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">وضعیت تاییدها</h6>
|
|
||||||
{% if can_approve_reject %}
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveFinalSettleModal" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectFinalSettleModal">رد</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body py-3">
|
|
||||||
<div class="row g-2">
|
|
||||||
{% for st in approver_statuses %}
|
|
||||||
<div class="col-12 col-md-6 col-lg-4">
|
|
||||||
<div class="d-flex flex-column border rounded px-2 py-1">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
|
|
||||||
{% if st.status == 'approved' %}
|
|
||||||
<span class="badge bg-success">تایید شد</span>
|
|
||||||
{% elif st.status == 'rejected' %}
|
|
||||||
<span class="badge bg-danger">رد شد</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning text-dark">در انتظار</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if st.status == 'rejected' and st.reason %}
|
|
||||||
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="col-12 d-flex justify-content-between mt-3">
|
|
||||||
{% if previous_step %}
|
{% if previous_step %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
|
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if step_instance.status == 'completed' %}
|
<button type="button" id="btnApproveFinalSettlement" class="btn btn-primary">تایید و ادامه</button>
|
||||||
{% if next_step %}
|
</div>
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal (final settlement payments) -->
|
|
||||||
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف تراکنش</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
آیا از حذف این تراکنش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Approve Final Settlement Modal -->
|
|
||||||
<div class="modal fade" id="approveFinalSettleModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="approve">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">تایید تسویه نهایی</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{% if invoice.remaining_amount != 0 %}
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
مانده فاکتور صفر نیست: <strong>{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</strong><br>
|
|
||||||
تا صفر نشود امکان تایید نیست.
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
آیا از تایید این مرحله اطمینان دارید؟
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-success" {% if invoice.remaining_amount != 0 %}disabled{% endif %}>تایید</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reject Final Settlement Modal -->
|
|
||||||
<div class="modal fade" id="rejectFinalSettleModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="reject">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">رد تسویه نهایی</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">علت رد</label>
|
|
||||||
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-danger">ثبت رد</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
@ -332,10 +191,7 @@
|
||||||
if (g) { fd.set('payment_date', g); }
|
if (g) { fd.set('payment_date', g); }
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
(function(){
|
document.getElementById('btnAddFinalPayment').addEventListener('click', function(){
|
||||||
const btn = document.getElementById('btnAddFinalPayment');
|
|
||||||
if (!btn) return;
|
|
||||||
btn.addEventListener('click', function(){
|
|
||||||
const fd = buildForm();
|
const fd = buildForm();
|
||||||
// Frontend validation
|
// Frontend validation
|
||||||
const amount = document.getElementById('id_amount').value.trim();
|
const amount = document.getElementById('id_amount').value.trim();
|
||||||
|
@ -358,19 +214,11 @@
|
||||||
}
|
}
|
||||||
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|
||||||
let deleteTargetId = null;
|
function deleteFinalPayment(id){
|
||||||
function openDeleteModal(id){
|
|
||||||
deleteTargetId = id;
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('deletePaymentModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
function confirmDeletePayment(){
|
|
||||||
if (!deleteTargetId) return;
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
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=>{
|
.then(r=>r.json()).then(resp=>{
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
showToast('حذف شد', 'success');
|
showToast('حذف شد', 'success');
|
||||||
|
@ -381,7 +229,20 @@
|
||||||
}).catch(()=> showToast('خطا در ارتباط با سرور', 'danger'));
|
}).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'));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load processes_tags %}
|
{% load processes_tags %}
|
||||||
{% load accounts_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
|
@ -56,15 +55,14 @@
|
||||||
<div class="content active dstepper-block">
|
<div class="content active dstepper-block">
|
||||||
<div class="content-header mb-3">
|
<div class="content-header mb-3">
|
||||||
<h6 class="mb-0">{{ step.name }}</h6>
|
<h6 class="mb-0">{{ step.name }}</h6>
|
||||||
<small>ثبت فیشها/چکهای واریزی برای پیشفاکتور</small>
|
<small>ثبت فیشهای واریزی برای پیشفاکتور</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
{% if can_manage_payments %}
|
|
||||||
<div class="col-12 col-lg-5">
|
<div class="col-12 col-lg-5">
|
||||||
<div class="card h-100 border">
|
<div class="card h-100 border">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">ثبت فیش/چک جدید</h5>
|
<h5 class="card-title mb-0">ثبت فیش جدید</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -86,11 +84,11 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">شماره مرجع/چک</label>
|
<label class="form-label">شماره مرجع</label>
|
||||||
<input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
|
<input type="text" class="form-control" name="reference_number" id="id_reference_number" placeholder="..." required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">تصویر فیش/چک</label>
|
<label class="form-label">تصویر فیش</label>
|
||||||
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
<input type="file" class="form-control" name="receipt_image" id="id_receipt_image" accept="image/*" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -98,16 +96,16 @@
|
||||||
<textarea class="form-control" rows="2" name="notes" id="id_notes"></textarea>
|
<textarea class="form-control" rows="2" name="notes" id="id_notes"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="button" id="btnAddPayment" class="btn btn-primary">افزودن فیش/چک</button>
|
<button type="button" id="btnAddPayment" class="btn btn-primary">افزودن فیش</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="col-12 col-lg-7">
|
||||||
<div class="col-12 {% if can_manage_payments %}col-lg-7{% else %}col-lg-12{% endif %}">
|
|
||||||
<div class="card mb-3 border">
|
<div class="card mb-3 border">
|
||||||
<div class="card-header d-flex justify-content-between">
|
<div class="card-header d-flex justify-content-between">
|
||||||
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
||||||
|
<a href="{% url 'invoices:quote_preview_step' instance.id step.id|add:'-1' %}" class="btn btn-sm btn-label-secondary">مشاهده پیشفاکتور</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
@ -141,10 +139,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border">
|
<div class="card border">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<div>
|
<h5 class="card-title mb-0">فیشهای ثبت شده</h5>
|
||||||
<h5 class="card-title mb-0">فیشها/چکهای ثبت شده</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped mb-0">
|
<table class="table table-striped mb-0">
|
||||||
|
@ -153,8 +149,9 @@
|
||||||
<th>مبلغ</th>
|
<th>مبلغ</th>
|
||||||
<th>تاریخ</th>
|
<th>تاریخ</th>
|
||||||
<th>روش</th>
|
<th>روش</th>
|
||||||
<th>شماره مرجع/چک</th>
|
<th>شماره مرجع</th>
|
||||||
<th>عملیات</th>
|
<th>تصویر</th>
|
||||||
|
<th style="width:120px">عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -165,23 +162,28 @@
|
||||||
<td>{{ p.get_payment_method_display }}</td>
|
<td>{{ p.get_payment_method_display }}</td>
|
||||||
<td>{{ p.reference_number|default:'-' }}</td>
|
<td>{{ p.reference_number|default:'-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group">
|
|
||||||
{% if p.receipt_image %}
|
{% if p.receipt_image %}
|
||||||
<a href="{{ p.receipt_image.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="مشاهده" aria-label="مشاهده">
|
<a href="{{ p.receipt_image.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="مشاهده" aria-label="مشاهده">
|
||||||
<i class="bx bx-show"></i>
|
<i class="bx bx-show"></i>
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_manage_payments %}
|
</td>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف">
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="editPayment({{ p.id }})" title="ویرایش" aria-label="ویرایش">
|
||||||
|
<i class="bx bx-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal({{ p.id }})" title="حذف" aria-label="حذف">
|
||||||
<i class="bx bx-trash"></i>
|
<i class="bx bx-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center text-muted">تا کنون فیش/چکی ثبت نشده است</td>
|
<td colspan="6" class="text-center text-muted">تا کنون فیشی ثبت نشده است</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -189,42 +191,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if approver_statuses %}
|
|
||||||
<div class="card border mt-2">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">وضعیت تاییدها</h6>
|
|
||||||
{% if can_approve_reject %}
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approvePaymentsModal2" {% if step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body py-3">
|
|
||||||
<div class="row g-2">
|
|
||||||
{% for st in approver_statuses %}
|
|
||||||
<div class="col-12 col-md-6 col-lg-4">
|
|
||||||
<div class="d-flex flex-column border rounded px-2 py-1">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
|
|
||||||
{% if st.status == 'approved' %}
|
|
||||||
<span class="badge bg-success">تایید شد</span>
|
|
||||||
{% elif st.status == 'rejected' %}
|
|
||||||
<span class="badge bg-danger">رد شد</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning text-dark">در انتظار</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if st.status == 'rejected' and st.reason %}
|
|
||||||
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="col-12 d-flex justify-content-between mt-3">
|
<div class="col-12 d-flex justify-content-between mt-3">
|
||||||
{% if previous_step %}
|
{% if previous_step %}
|
||||||
|
@ -235,114 +201,30 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if step_instance.status == 'completed' %}
|
<button type="button" id="btnApprovePayments" class="btn btn-primary">
|
||||||
{% if next_step %}
|
تایید پرداختها
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
|
<i class="bx bx-chevron-left bx-sm ms-sm-2"></i>
|
||||||
<span class="align-middle d-sm-inline-block d-none me-sm-1">بعدی</span>
|
</button>
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
|
||||||
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف فیش</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
آیا از حذف این فیش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Removed legacy approvePaymentsModal; using approvePaymentsModal2 with form POST -->
|
|
||||||
<!-- Approve Modal 2 (direct approve button in header) -->
|
|
||||||
<div class="modal fade" id="approvePaymentsModal2" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="approve">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">تایید پرداختها</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{% if not totals.is_fully_paid %}
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
مبلغی از پیشفاکتور هنوز پرداخت نشده است.
|
|
||||||
<div class="mt-1">مانده: <strong>{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان</strong></div>
|
|
||||||
</div>
|
|
||||||
آیا مطمئن هستید که میخواهید مرحله را تایید کنید؟
|
|
||||||
{% else %}
|
|
||||||
آیا از تایید این مرحله اطمینان دارید؟
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-success">تایید</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Reject Modal for payments step -->
|
|
||||||
<div class="modal fade" id="rejectPaymentsModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="reject">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">رد مرحله پرداختها</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">علت رد</label>
|
|
||||||
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
|
|
||||||
<button type="submit" class="btn btn-danger">ثبت رد</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
// Removed legacy isFullyPaid-driven approve flow; approval now via modal submit
|
const isFullyPaid = {{ totals.is_fully_paid|yesno:'true,false' }};
|
||||||
function buildFormData(form) {
|
function buildFormData(form) {
|
||||||
const fd = new FormData(form);
|
const fd = new FormData(form);
|
||||||
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
const btnAddPayment = document.getElementById('btnAddPayment');
|
document.getElementById('btnAddPayment').addEventListener('click', function() {
|
||||||
if (btnAddPayment) btnAddPayment.addEventListener('click', function() {
|
|
||||||
// Front-end validation
|
// Front-end validation
|
||||||
const amount = document.getElementById('id_amount').value.trim();
|
const amount = document.getElementById('id_amount').value.trim();
|
||||||
const payDate = document.getElementById('id_payment_date').value.trim();
|
const payDate = document.getElementById('id_payment_date').value.trim();
|
||||||
|
@ -401,7 +283,51 @@
|
||||||
alert('ویرایش فیش را بعدا با مدال تکمیل میکنیم. فعلا حذف و افزودن مجدد انجام دهید.');
|
alert('ویرایش فیش را بعدا با مدال تکمیل میکنیم. فعلا حذف و افزودن مجدد انجام دهید.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy approve JS removed; approval handled by modal forms in header
|
function performApprovePayments() {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('csrfmiddlewaretoken', document.querySelector('input[name=csrfmiddlewaretoken]').value);
|
||||||
|
fetch('{% url "invoices:approve_payments" 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 || resp.error || 'خطا در تایید پرداختها', 'danger');
|
||||||
|
}
|
||||||
|
}).catch(() => showToast('خطا در ارتباط با سرور', 'danger'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openApproveModal() {
|
||||||
|
const el = document.getElementById('approvePaymentsModal');
|
||||||
|
const remEl = document.getElementById('remainingAmountText');
|
||||||
|
if (remEl) {
|
||||||
|
remEl.textContent = '{{ totals.remaining_amount|floatformat:0|intcomma:False }} تومان';
|
||||||
|
}
|
||||||
|
// Prefer jQuery plugin if available to avoid namespace issues
|
||||||
|
if (window.$ && typeof $(el).modal === 'function') {
|
||||||
|
$(el).modal('show');
|
||||||
|
} else if (window.bootstrap && window.bootstrap.Modal) {
|
||||||
|
const modal = new window.bootstrap.Modal(el);
|
||||||
|
modal.show();
|
||||||
|
} else {
|
||||||
|
// fallback: force display
|
||||||
|
el.classList.add('show');
|
||||||
|
el.style.display = 'block';
|
||||||
|
el.removeAttribute('aria-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnApprovePayments').addEventListener('click', function() {
|
||||||
|
if (isFullyPaid) {
|
||||||
|
performApprovePayments();
|
||||||
|
} else {
|
||||||
|
openApproveModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Persian Date Picker JS -->
|
<!-- Persian Date Picker JS -->
|
||||||
|
@ -439,4 +365,42 @@
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deletePaymentModal" tabindex="-1" aria-labelledby="deletePaymentModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deletePaymentModalLabel">تایید حذف فیش</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
آیا از حذف این فیش مطمئن هستید؟ این عمل قابل بازگشت نیست.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="confirmDeletePayment()" data-bs-dismiss="modal">حذف</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Approve Confirmation Modal (shown when remaining amount > 0) -->
|
||||||
|
<div class="modal fade" id="approvePaymentsModal" tabindex="-1" aria-labelledby="approvePaymentsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="approvePaymentsModalLabel">تایید نهایی پرداختها</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
مبلغی از پیشفاکتور هنوز پرداخت نشده است.
|
||||||
|
<div class="mt-2">مانده: <strong id="remainingAmountText"></strong></div>
|
||||||
|
آیا مطمئن هستید که میخواهید مرحله را تایید و به مرحله بعد بروید؟
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="performApprovePayments()">بله، تایید</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -221,7 +221,6 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_broker %}
|
|
||||||
{% if step_instance.status == 'completed' %}
|
{% if step_instance.status == 'completed' %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
||||||
|
@ -237,17 +236,6 @@
|
||||||
تایید پیشفاکتور
|
تایید پیشفاکتور
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
{% if next_step %}
|
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}"
|
|
||||||
class="btn btn-label-primary">
|
|
||||||
مرحله بعد
|
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,7 +58,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{% if is_broker or existing_quote %}
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm align-middle">
|
<table class="table table-sm align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -78,8 +77,7 @@
|
||||||
data-item-id="{{ item.id }}"
|
data-item-id="{{ item.id }}"
|
||||||
data-is-default="{% if item.is_default_in_quotes %}1{% else %}0{% endif %}"
|
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 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 %}disabled title="آیتم پیشفرض است و قابل حذف نیست"{% endif %}>
|
||||||
{% if item.is_default_in_quotes %}title="آیتم پیشفرض است و قابل حذف نیست"{% elif not is_broker %}title="فقط کارگزار مجاز به تغییر اقلام است"{% endif %}>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
|
@ -88,6 +86,7 @@
|
||||||
<span class="badge bg-label-primary me-2">پیشفرض</span>
|
<span class="badge bg-label-primary me-2">پیشفرض</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if item.description %}<small class="text-muted">{{ item.description }}</small>{% endif %}
|
{% if item.description %}<small class="text-muted">{{ item.description }}</small>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -95,8 +94,7 @@
|
||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm quote-item-qty" min="1"
|
<input type="number" class="form-control form-control-sm quote-item-qty" min="1"
|
||||||
data-item-id="{{ item.id }}"
|
data-item-id="{{ item.id }}"
|
||||||
value="{% if selected_qty %}{{ selected_qty }}{% else %}{{ item.default_quantity }}{% endif %}"
|
value="{% if selected_qty %}{{ selected_qty }}{% else %}{{ item.default_quantity }}{% endif %}">
|
||||||
{% if not is_broker %}disabled title="فقط کارگزار مجاز به تغییر تعداد است"{% endif %}>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -104,9 +102,8 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-warning mb-0">شما دسترسی به ثبت اقلام ندارید.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 d-flex justify-content-between">
|
<div class="col-12 d-flex justify-content-between">
|
||||||
|
@ -121,36 +118,28 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if is_broker %}
|
|
||||||
{% if step_instance.status == 'completed' %}
|
{% if step_instance.status == 'completed' %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
<div class="d-flex justify-content-end mt-3">
|
<div class="d-flex justify-content-end mt-3">
|
||||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||||
|
|
||||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||||
و بعدی
|
و بعدی
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-success" type="button">اتمام</button>
|
<button class="btn btn-success" type="button">اتمام</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
<button type="button" class="btn btn-primary" id="btnCreateQuote">
|
||||||
|
|
||||||
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
{% if existing_quote %}بروزرسانی پیشفاکتور{% else %}ثبت پیشفاکتور{% endif %}
|
||||||
و بعدی
|
و بعدی
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
{% if next_step %}
|
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-label-primary">
|
|
||||||
مرحله بعد
|
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-success">اتمام</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,9 +9,7 @@ from django.urls import reverse
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from processes.models import ProcessInstance, ProcessStep, StepInstance, StepRejection, StepApproval
|
from processes.models import ProcessInstance, ProcessStep, StepInstance
|
||||||
from accounts.models import Role
|
|
||||||
from common.consts import UserRoles
|
|
||||||
from .models import Item, Quote, QuoteItem, Payment, Invoice
|
from .models import Item, Quote, QuoteItem, Payment, Invoice
|
||||||
from installations.models import InstallationReport, InstallationItemChange
|
from installations.models import InstallationReport, InstallationItemChange
|
||||||
|
|
||||||
|
@ -30,7 +28,7 @@ def quote_step(request, instance_id, step_id):
|
||||||
return redirect('processes:request_list')
|
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 = Quote.objects.filter(process_instance=instance).first()
|
||||||
existing_quote_items = {}
|
existing_quote_items = {}
|
||||||
if existing_quote:
|
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()
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
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', {
|
return render(request, 'invoices/quote_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
'step': step,
|
'step': step,
|
||||||
|
@ -59,7 +49,6 @@ def quote_step(request, instance_id, step_id):
|
||||||
'existing_quote': existing_quote,
|
'existing_quote': existing_quote,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_step,
|
'next_step': next_step,
|
||||||
'is_broker': is_broker,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
|
@ -68,13 +57,6 @@ def create_quote(request, instance_id, step_id):
|
||||||
"""ساخت/بروزرسانی پیشفاکتور از اقلام انتخابی"""
|
"""ساخت/بروزرسانی پیشفاکتور از اقلام انتخابی"""
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
step = get_object_or_404(instance.process.steps, id=step_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:
|
try:
|
||||||
items_payload = json.loads(request.POST.get('items') or '[]')
|
items_payload = json.loads(request.POST.get('items') or '[]')
|
||||||
|
@ -90,7 +72,7 @@ def create_quote(request, instance_id, step_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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:
|
if default_item_ids:
|
||||||
for default_id in default_item_ids:
|
for default_id in default_item_ids:
|
||||||
if default_id not in payload_by_id:
|
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()
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
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', {
|
return render(request, 'invoices/quote_preview_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
'step': step,
|
'step': step,
|
||||||
|
@ -196,7 +170,6 @@ def quote_preview_step(request, instance_id, step_id):
|
||||||
'quote': quote,
|
'quote': quote,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_step,
|
'next_step': next_step,
|
||||||
'is_broker': is_broker,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -217,13 +190,6 @@ def approve_quote(request, instance_id, step_id):
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||||
quote = get_object_or_404(Quote, process_instance=instance)
|
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'
|
quote.status = 'sent'
|
||||||
|
@ -281,97 +247,7 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
'is_fully_paid': quote.get_remaining_amount() <= 0,
|
'is_fully_paid': quote.get_remaining_amount() <= 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
|
step_instance = instance.step_instances.filter(step=step).first()
|
||||||
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
|
|
||||||
|
|
||||||
return render(request, 'invoices/quote_payment_step.html', {
|
return render(request, 'invoices/quote_payment_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
@ -382,12 +258,6 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
'totals': totals,
|
'totals': totals,
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_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__)
|
logger = logging.getLogger(__name__)
|
||||||
try:
|
try:
|
||||||
amount = (request.POST.get('amount') or '').strip()
|
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)
|
logger.exception('Error adding quote payment (instance=%s, step=%s)', instance_id, step_id)
|
||||||
return JsonResponse({'success': False, 'message': 'خطا در ثبت فیش', 'error': str(e)})
|
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])
|
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
return JsonResponse({'success': True, 'redirect': redirect_url})
|
||||||
|
|
||||||
|
@ -509,15 +360,6 @@ def update_quote_payment(request, instance_id, step_id, payment_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
|
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])
|
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
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:
|
if not invoice:
|
||||||
return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
|
return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
|
||||||
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
|
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:
|
try:
|
||||||
# soft delete using project's BaseModel delete override
|
# soft delete using project's BaseModel delete override
|
||||||
payment.delete()
|
payment.delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
return JsonResponse({'success': False, 'message': 'خطا در حذف فیش'})
|
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])
|
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
||||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
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
|
# Choices for special items from DB
|
||||||
special_choices = list(Item.objects.filter(is_special=True).values('id', 'name'))
|
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', {
|
return render(request, 'invoices/final_invoice_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
'step': step,
|
'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(),
|
'invoice_specials': invoice.items.select_related('item').filter(item__is_special=True, is_deleted=False).all(),
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_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)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
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)
|
# Block approval when there is any remaining (positive or negative)
|
||||||
invoice.calculate_totals()
|
invoice.calculate_totals()
|
||||||
# if invoice.remaining_amount != 0:
|
# 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)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
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
|
# charge_type was removed from UI; we no longer require it
|
||||||
item_id = request.POST.get('item_id')
|
item_id = request.POST.get('item_id')
|
||||||
amount = (request.POST.get('amount') or '').strip()
|
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):
|
def delete_special_charge(request, instance_id, step_id, item_id):
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
invoice = get_object_or_404(Invoice, process_instance=instance)
|
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
|
from .models import InvoiceItem
|
||||||
inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice)
|
inv_item = get_object_or_404(InvoiceItem, id=item_id, invoice=invoice)
|
||||||
# allow deletion only for special items
|
# 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()
|
previous_step = instance.process.steps.filter(order__lt=step.order).last()
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
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', {
|
return render(request, 'invoices/final_settlement_step.html', {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
'step': step,
|
'step': step,
|
||||||
'invoice': invoice,
|
'invoice': invoice,
|
||||||
'payments': invoice.payments.filter(is_deleted=False).all(),
|
'payments': invoice.payments.filter(is_deleted=False).all(),
|
||||||
'step_instance': step_instance,
|
|
||||||
'previous_step': previous_step,
|
'previous_step': previous_step,
|
||||||
'next_step': next_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
|
@login_required
|
||||||
def add_final_payment(request, instance_id, step_id):
|
def add_final_payment(request, instance_id, step_id):
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_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)
|
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()
|
amount = (request.POST.get('amount') or '').strip()
|
||||||
payment_date = (request.POST.get('payment_date') or '').strip()
|
payment_date = (request.POST.get('payment_date') or '').strip()
|
||||||
payment_method = (request.POST.get('payment_method') 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.
|
# After creation, totals auto-updated by model save. Respond with redirect and new totals for UX.
|
||||||
invoice.refresh_from_db()
|
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({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]),
|
'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
|
@login_required
|
||||||
def delete_final_payment(request, instance_id, step_id, payment_id):
|
def delete_final_payment(request, instance_id, step_id, payment_id):
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_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)
|
invoice = get_object_or_404(Invoice, process_instance=instance)
|
||||||
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
|
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()
|
payment.delete()
|
||||||
invoice.refresh_from_db()
|
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': {
|
return JsonResponse({'success': True, 'redirect': reverse('invoices:final_settlement_step', args=[instance.id, step_id]), 'totals': {
|
||||||
'final_amount': str(invoice.final_amount),
|
'final_amount': str(invoice.final_amount),
|
||||||
'paid_amount': str(invoice.paid_amount),
|
'paid_amount': str(invoice.paid_amount),
|
||||||
|
|
|
@ -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 django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
from simple_history.admin import SimpleHistoryAdmin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
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)
|
@admin.register(Process)
|
||||||
class ProcessAdmin(SimpleHistoryAdmin):
|
class ProcessAdmin(SimpleHistoryAdmin):
|
||||||
|
@ -168,16 +168,14 @@ class StepRejectionAdmin(SimpleHistoryAdmin):
|
||||||
return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason
|
return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason
|
||||||
reason_short.short_description = "دلیل رد شدن"
|
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)
|
def changes_short(self, obj):
|
||||||
class StepApproverRequirementAdmin(admin.ModelAdmin):
|
return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description
|
||||||
list_display = ("step", "role", "required_count")
|
changes_short.short_description = "تغییرات"
|
||||||
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")
|
|
||||||
|
|
|
@ -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})
|
||||||
|
}
|
|
@ -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 django.db.models.deletion
|
||||||
import simple_history.models
|
import simple_history.models
|
||||||
|
@ -11,7 +11,6 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0001_initial'),
|
|
||||||
('wells', '0001_initial'),
|
('wells', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
@ -232,17 +231,42 @@ class Migration(migrations.Migration):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='StepApproverRequirement',
|
name='HistoricalStepRevision',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||||
('required_count', models.PositiveIntegerField(default=1, verbose_name='تعداد موردنیاز')),
|
('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
|
||||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.role', verbose_name='نقش تاییدکننده')),
|
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ اصلاح')),
|
||||||
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approver_requirements', to='processes.processstep', 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={
|
options={
|
||||||
'verbose_name': 'نیازمندی تایید نقش',
|
'verbose_name': 'historical بازبینی مرحله',
|
||||||
'verbose_name_plural': 'نیازمندی\u200cهای تایید نقش',
|
'verbose_name_plural': 'historical بازبینی\u200cهای مراحل',
|
||||||
'unique_together': {('step', 'role')},
|
'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(
|
migrations.CreateModel(
|
||||||
|
@ -260,21 +284,4 @@ class Migration(migrations.Migration):
|
||||||
'unique_together': {('dependent_step', 'dependency_step')},
|
'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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,8 +4,6 @@ from common.models import NameSlugModel, SluggedModel
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
|
||||||
from accounts.models import Role
|
|
||||||
from _helpers.utils import generate_unique_slug
|
from _helpers.utils import generate_unique_slug
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
@ -48,9 +46,6 @@ class ProcessStep(NameSlugModel):
|
||||||
)
|
)
|
||||||
history = HistoricalRecords()
|
history = HistoricalRecords()
|
||||||
|
|
||||||
# Note: approver requirements are defined via StepApproverRequirement through model
|
|
||||||
# See StepApproverRequirement below
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "مرحله فرآیند"
|
verbose_name = "مرحله فرآیند"
|
||||||
verbose_name_plural = "مراحل فرآیند"
|
verbose_name_plural = "مراحل فرآیند"
|
||||||
|
@ -68,7 +63,6 @@ class ProcessStep(NameSlugModel):
|
||||||
"""دریافت مراحلی که به این مرحله وابسته هستند"""
|
"""دریافت مراحلی که به این مرحله وابسته هستند"""
|
||||||
return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
|
return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
|
||||||
|
|
||||||
|
|
||||||
class StepDependency(models.Model):
|
class StepDependency(models.Model):
|
||||||
"""مدل وابستگی بین مراحل"""
|
"""مدل وابستگی بین مراحل"""
|
||||||
dependent_step = models.ForeignKey(
|
dependent_step = models.ForeignKey(
|
||||||
|
@ -245,7 +239,7 @@ class ProcessInstance(SluggedModel):
|
||||||
'cancelled': 'warning',
|
'cancelled': 'warning',
|
||||||
}
|
}
|
||||||
color = status_colors.get(self.status, 'secondary')
|
color = status_colors.get(self.status, 'secondary')
|
||||||
return '<span class="badge bg-label-{}">{}</span>'.format(color, self.get_status_display())
|
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
|
||||||
|
|
||||||
def get_priority_display_with_color(self):
|
def get_priority_display_with_color(self):
|
||||||
"""نمایش اولویت با رنگ"""
|
"""نمایش اولویت با رنگ"""
|
||||||
|
@ -296,7 +290,6 @@ class ProcessInstance(SluggedModel):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class StepInstance(models.Model):
|
class StepInstance(models.Model):
|
||||||
"""مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
|
"""مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
|
||||||
process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند")
|
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()
|
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):
|
class StepRejection(models.Model):
|
||||||
"""مدل رد شدن مرحله"""
|
"""مدل رد شدن مرحله"""
|
||||||
step_instance = models.ForeignKey(
|
step_instance = models.ForeignKey(
|
||||||
|
@ -418,35 +390,37 @@ class StepRejection(models.Model):
|
||||||
self.step_instance.save()
|
self.step_instance.save()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class StepRevision(models.Model):
|
||||||
class StepApproverRequirement(models.Model):
|
"""مدل بازبینی و اصلاح مرحله"""
|
||||||
"""Required approver roles for a step."""
|
step_instance = models.ForeignKey(
|
||||||
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, related_name='approver_requirements', verbose_name="مرحله")
|
StepInstance,
|
||||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name="نقش تاییدکننده")
|
on_delete=models.CASCADE,
|
||||||
required_count = models.PositiveIntegerField(default=1, verbose_name="تعداد موردنیاز")
|
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:
|
class Meta:
|
||||||
unique_together = ('step', 'role')
|
verbose_name = "بازبینی مرحله"
|
||||||
verbose_name = "نیازمندی تایید نقش"
|
verbose_name_plural = "بازبینیهای مراحل"
|
||||||
verbose_name_plural = "نیازمندیهای تایید نقش"
|
ordering = ['-created_at']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.step} ← {self.role} (x{self.required_count})"
|
return f"بازبینی {self.step_instance} توسط {self.revised_by.get_full_name()}"
|
||||||
|
|
||||||
|
|
||||||
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}"
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
<div class="step
|
<div class="step
|
||||||
{% if not can_access %}disabled
|
{% if not can_access %}disabled
|
||||||
{% elif status == 'completed' %}completed
|
{% elif status == 'completed' %}completed
|
||||||
{% elif status == 'rejected' %}rejected
|
|
||||||
{% elif is_todo %}active
|
{% elif is_todo %}active
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_selected %} selected{% endif %}"
|
{% if is_selected %} selected{% endif %}"
|
||||||
|
@ -20,9 +19,9 @@
|
||||||
<span class="step-trigger">
|
<span class="step-trigger">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="bs-stepper-circle {% if status == 'rejected' %}bg-danger text-white{% endif %}">{{ forloop.counter }}</span>
|
<span class="bs-stepper-circle">{{ forloop.counter }}</span>
|
||||||
<span class="bs-stepper-label mt-1">
|
<span class="bs-stepper-label mt-1">
|
||||||
<span class="bs-stepper-title {% if status == 'rejected' %}text-danger{% endif %}">{{ step.name }}</span>
|
<span class="bs-stepper-title">{{ step.name }}</span>
|
||||||
<span class="bs-stepper-subtitle">{{ step.description|default:' ' }}</span>
|
<span class="bs-stepper-subtitle">{{ step.description|default:' ' }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
@ -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' %}
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mb-4">
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
||||||
<div>
|
|
||||||
<h4 class="mb-1">گزارش نهایی درخواست {{ instance.code }}</h4>
|
|
||||||
<small class="text-muted d-block">
|
|
||||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
|
||||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
{% if invoice %}
|
|
||||||
<a href="{% url 'invoices:final_invoice_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت فاکتور</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'certificates:certificate_print' instance.id %}" target="_blank" class="btn btn-outline-secondary"><i class="bx bx-printer"></i> پرینت گواهی</a>
|
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">فاکتور نهایی</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if invoice %}
|
|
||||||
<div class="row g-3 mb-3">
|
|
||||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مبلغ نهایی</div><div class="h5 mt-1">{{ invoice.final_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
|
||||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">پرداختیها</div><div class="h5 mt-1 text-success">{{ invoice.paid_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
|
||||||
<div class="col-6 col-md-3"><div class="border rounded p-3 h-100"><div class="small text-muted">مانده</div><div class="h5 mt-1 {% if invoice.remaining_amount <= 0 %}text-success{% else %}text-danger{% endif %}">{{ invoice.remaining_amount|floatformat:0|intcomma:False }} تومان</div></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>آیتم</th>
|
|
||||||
<th class="text-center">تعداد</th>
|
|
||||||
<th class="text-end">قیمت واحد</th>
|
|
||||||
<th class="text-end">قیمت کل</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for it in rows %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ it.item.name }}</td>
|
|
||||||
<td class="text-center">{{ it.quantity }}</td>
|
|
||||||
<td class="text-end">{{ it.unit_price|floatformat:0|intcomma:False }}</td>
|
|
||||||
<td class="text-end">{{ it.total_price|floatformat:0|intcomma:False }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="4" class="text-center text-muted">اطلاعاتی ندارد</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-muted">فاکتور نهایی ثبت نشده است.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">گزارش نصب</h6>
|
|
||||||
{% if latest_report and latest_report.assignment and latest_report.assignment.installer %}
|
|
||||||
<span class="small text-muted">نصاب: {{ latest_report.assignment.installer.get_full_name|default:latest_report.assignment.installer.username }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if latest_report %}
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ latest_report.visited_date|to_jalali|default:'-' }}</p>
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال کنتور جدید: {{ latest_report.new_water_meter_serial|default:'-' }}</p>
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ latest_report.seal_number|default:'-' }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ latest_report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ latest_report.utm_x|default:'-' }}</p>
|
|
||||||
<p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ latest_report.utm_y|default:'-' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if latest_report.description %}
|
|
||||||
<div class="mt-2">
|
|
||||||
<p class="mb-0"><i class="bx bx-text bx-sm me-2"></i><strong>توضیحات:</strong></p>
|
|
||||||
<div class="text-muted">{{ latest_report.description }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<hr>
|
|
||||||
<h6>عکسها</h6>
|
|
||||||
<div class="row">
|
|
||||||
{% for p in latest_report.photos.all %}
|
|
||||||
<div class="col-6 col-md-3 mb-2"><img class="img-fluid rounded border" src="{{ p.image.url }}" alt="photo"></div>
|
|
||||||
{% empty %}
|
|
||||||
<div class="text-muted">بدون عکس</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-muted">گزارش نصب ثبت نشده است.</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="mb-0">تراکنشها</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>نوع</th>
|
|
||||||
<th>مبلغ</th>
|
|
||||||
<th>تاریخ</th>
|
|
||||||
<th>روش</th>
|
|
||||||
<th>شماره مرجع/چک</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for p in payments %}
|
|
||||||
<tr>
|
|
||||||
<td>{% if p.direction == 'in' %}<span class="badge bg-success">دریافتی{% else %}<span class="badge bg-warning text-dark">پرداختی{% endif %}</span></td>
|
|
||||||
<td>{{ p.amount|floatformat:0|intcomma:False }} تومان</td>
|
|
||||||
<td>{{ p.payment_date|date:'Y/m/d' }}</td>
|
|
||||||
<td>{{ p.get_payment_method_display }}</td>
|
|
||||||
<td>{{ p.reference_number|default:'-' }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="5" class="text-center text-muted">بدون تراکنش</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
|
@ -28,113 +28,17 @@
|
||||||
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
|
|
||||||
<div class="row py-3 mb-4 card-header flex-column flex-md-row pb-0">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<div class="d-md-flex justify-content-between align-items-center dt-layout-start col-md-auto me-auto mt-0">
|
<h4 class="mb-0">درخواستها</h4>
|
||||||
<h5 class="card-title mb-0 text-md-start text-center fw-bold">لیست درخواستها</h5>
|
|
||||||
</div>
|
|
||||||
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
|
|
||||||
<div class="dt-buttons btn-group flex-wrap mb-0">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn buttons-collection btn-label-primary dropdown-toggle me-4 d-none" type="button">
|
|
||||||
<span>
|
|
||||||
<span class="d-flex align-items-center gap-2">
|
|
||||||
<i class="icon-base bx bx-export me-sm-1"></i>
|
|
||||||
<span class="d-none d-sm-inline-block">خروجی</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
|
||||||
<i class="bx bx-plus me-1"></i>
|
<i class="bx bx-plus"></i>
|
||||||
درخواست جدید
|
درخواست جدید
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Summary Cards -->
|
|
||||||
<div class="row g-4 mb-4">
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-start justify-content-between">
|
|
||||||
<div class="content-left">
|
|
||||||
<span>کل درخواستها</span>
|
|
||||||
<div class="d-flex align-items-end mt-2">
|
|
||||||
<h4 class="mb-0 me-2">{{ total_count }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="avatar">
|
|
||||||
<span class="avatar-initial rounded bg-label-primary">
|
|
||||||
<i class="bx bx-list-ul bx-sm"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-start justify-content-between">
|
|
||||||
<div class="content-left">
|
|
||||||
<span>تکمیلشده</span>
|
|
||||||
<div class="d-flex align-items-end mt-2">
|
|
||||||
<h4 class="mb-0 me-2">{{ completed_count }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="avatar">
|
|
||||||
<span class="avatar-initial rounded bg-label-success">
|
|
||||||
<i class="bx bx-badge-check bx-sm"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-start justify-content-between">
|
|
||||||
<div class="content-left">
|
|
||||||
<span>در حال انجام</span>
|
|
||||||
<div class="d-flex align-items-end mt-2">
|
|
||||||
<h4 class="mb-0 me-2">{{ in_progress_count }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="avatar">
|
|
||||||
<span class="avatar-initial rounded bg-label-info">
|
|
||||||
<i class="bx bx-loader-circle bx-sm"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 col-xl-3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-start justify-content-between">
|
|
||||||
<div class="content-left">
|
|
||||||
<span>در انتظار</span>
|
|
||||||
<div class="d-flex align-items-end mt-2">
|
|
||||||
<h4 class="mb-0 me-2">{{ pending_count }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="avatar">
|
|
||||||
<span class="avatar-initial rounded bg-label-warning">
|
|
||||||
<i class="bx bx-time bx-sm"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-datatable table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="requests-table" class="datatables-basic table border-top">
|
<table id="requestsTable" class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>شناسه</th>
|
<th>شناسه</th>
|
||||||
|
@ -142,43 +46,25 @@
|
||||||
<th>مرحله فعلی</th>
|
<th>مرحله فعلی</th>
|
||||||
<th>شماره اشتراک آب</th>
|
<th>شماره اشتراک آب</th>
|
||||||
<th>نماینده</th>
|
<th>نماینده</th>
|
||||||
<th>استان</th>
|
<th>درخواستکننده</th>
|
||||||
<th>امور</th>
|
<th>اولویت</th>
|
||||||
<th>پیشرفت</th>
|
|
||||||
<th>وضعیت</th>
|
<th>وضعیت</th>
|
||||||
<th>تاریخ ایجاد</th>
|
<th>تاریخ ایجاد</th>
|
||||||
<th>عملیات</th>
|
<th>عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in instances_with_progress %}
|
{% for inst in instances %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ item.instance.code }}</td>
|
<td>{{ inst.code }}</td>
|
||||||
<td>{{ item.instance.process.name }}</td>
|
<td>{{ inst.process.name }}</td>
|
||||||
<td class="text-primary">
|
<td class="text-primary">{{ inst.current_step.name|default:"--" }}</td>
|
||||||
{% if item.instance.status == 'completed' %}
|
<td>{{ inst.well.water_subscription_number }}</td>
|
||||||
<a href="{% url 'processes:instance_summary' item.instance.id %}" class="text-primary">{{ item.instance.current_step.name|default:"--" }}</a>
|
<td>{% if inst.representative %}{{ inst.representative.get_full_name }}{% else %}-{% endif %}</td>
|
||||||
{% elif item.instance.current_step %}
|
<td>{% if inst.requester %}{{ inst.requester.get_full_name }}{% else %}-{% endif %}</td>
|
||||||
<a href="{% url 'processes:instance_steps' item.instance.id %}" class="text-primary">{{ item.instance.current_step.name }}</a>
|
<td>{{ inst.get_priority_display }}</td>
|
||||||
{% else %}
|
<td>{{ inst.get_status_display }}</td>
|
||||||
--
|
<td>{{ inst.jcreated }}</td>
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ item.instance.well.water_subscription_number }}</td>
|
|
||||||
<td>{% if item.instance.representative %}{{ item.instance.representative.get_full_name }}{% else %}-{% endif %}</td>
|
|
||||||
<td>{% if item.instance.well and item.instance.well.county %}{{ item.instance.well.county }}{% else %}-{% endif %}</td>
|
|
||||||
<td>{% if item.instance.well and item.instance.well.affairs %}{{ item.instance.well.affairs }}{% else %}-{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="progress me-2" style="width: 80px; height: 6px;">
|
|
||||||
<div class="progress-bar {% if item.progress_percentage == 100 %}bg-success{% elif item.progress_percentage >= 70 %}bg-info{% elif item.progress_percentage >= 40 %}bg-warning{% else %}bg-secondary{% endif %}" role="progressbar" style="width: {{ item.progress_percentage }}%;" aria-valuenow="{{ item.progress_percentage }}" aria-valuemin="0" aria-valuemax="100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">{{ item.progress_percentage }}%</small>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ item.instance.get_status_display_with_color|safe }}</td>
|
|
||||||
<td>{{ item.instance.jcreated }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-inline-block">
|
<div class="d-inline-block">
|
||||||
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
|
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
|
||||||
|
@ -186,19 +72,13 @@
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end m-0">
|
<ul class="dropdown-menu dropdown-menu-end m-0">
|
||||||
<li>
|
<li>
|
||||||
{% if item.instance.status == 'completed' %}
|
<a href="{% url 'processes:instance_steps' inst.id %}" class="dropdown-item">
|
||||||
<a href="{% url 'processes:instance_summary' item.instance.id %}" class="dropdown-item">
|
|
||||||
<i class="bx bx-show me-1"></i>مشاهده گزارش
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'processes:instance_steps' item.instance.id %}" class="dropdown-item">
|
|
||||||
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="dropdown-item text-danger" data-instance-id="{{ item.instance.id }}" data-instance-code="{{ item.instance.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
|
<a href="#" class="dropdown-item text-danger" data-instance-id="{{ inst.id }}" data-instance-code="{{ inst.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
|
||||||
<i class="bx bx-trash me-1"></i>حذف
|
<i class="bx bx-trash me-1"></i>حذف
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -208,7 +88,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="11" class="text-center text-muted">موردی ثبت نشده است</td>
|
<td colspan="9" class="text-center text-muted">موردی ثبت نشده است</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -240,7 +120,7 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="form-label">شماره اشتراک آب</label>
|
<label class="form-label">شماره اشتراک آب</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="req_water_sub" name="water_subscription_number" data-field="water_subscription_number" placeholder="مثال: 12345" required>
|
<input type="text" class="form-control" id="req_water_sub" placeholder="مثال: 12345" required>
|
||||||
<button class="btn btn-outline-secondary" type="button" id="btnLookupWell">
|
<button class="btn btn-outline-secondary" type="button" id="btnLookupWell">
|
||||||
بررسی/افزودن چاه
|
بررسی/افزودن چاه
|
||||||
</button>
|
</button>
|
||||||
|
@ -331,7 +211,7 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="form-label">کد ملی نماینده</label>
|
<label class="form-label">کد ملی نماینده</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="rep_national_code" data-field="national_code" placeholder="مثال: 0012345678" maxlength="10" inputmode="numeric" pattern="\d*">
|
<input type="text" class="form-control" id="rep_national_code" placeholder="مثال: 0012345678">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="btnLookupRep">
|
<button class="btn btn-outline-secondary" type="button" id="btnLookupRep">
|
||||||
بررسی/افزودن نماینده
|
بررسی/افزودن نماینده
|
||||||
</button>
|
</button>
|
||||||
|
@ -382,7 +262,7 @@
|
||||||
<hr class="mt-3 border border-dashed">
|
<hr class="mt-3 border border-dashed">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="form-label">توضیحات</label>
|
<label class="form-label">توضیحات</label>
|
||||||
<textarea class="form-control" rows="3" id="req_description" name="description"></textarea>
|
<textarea class="form-control" rows="3" id="req_description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -475,13 +355,19 @@
|
||||||
|
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
// Initialize DataTable similar to customer_list
|
// if ($.fn.DataTable) {
|
||||||
$('#requests-table').DataTable({
|
// try {
|
||||||
pageLength: 10,
|
// $('#requestsTable').DataTable({
|
||||||
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
|
// pageLength: 10,
|
||||||
order: [[0, 'desc']],
|
// order: [[0, 'desc']]
|
||||||
responsive: true,
|
// });
|
||||||
});
|
// } catch (e) {
|
||||||
|
// console.error('DataTable init failed', e);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.warn('DataTables library not loaded');
|
||||||
|
// }
|
||||||
|
|
||||||
let currentWellId = null;
|
let currentWellId = null;
|
||||||
let currentRepId = null;
|
let currentRepId = null;
|
||||||
let wellChecked = false;
|
let wellChecked = false;
|
||||||
|
@ -507,7 +393,7 @@
|
||||||
if (!$el.length) return false;
|
if (!$el.length) return false;
|
||||||
$el.addClass('is-invalid');
|
$el.addClass('is-invalid');
|
||||||
const $feedback = $('<div class="invalid-feedback inline-error"></div>').text(message);
|
const $feedback = $('<div class="invalid-feedback inline-error"></div>').text(message);
|
||||||
const $grp = $el.closest('.input-group, .form-group, .mb-3');
|
const $grp = $el.closest('.input-group');
|
||||||
if ($grp.length) {
|
if ($grp.length) {
|
||||||
$feedback.insertAfter($grp);
|
$feedback.insertAfter($grp);
|
||||||
} else {
|
} else {
|
||||||
|
@ -516,49 +402,60 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic field resolution with small exception map
|
function mapWellFieldToSelector(field) {
|
||||||
const exceptionMap = {
|
switch (field) {
|
||||||
water_subscription_number: '#req_water_sub',
|
case 'water_subscription_number': return '#req_water_sub';
|
||||||
national_code: '#rep_national_code',
|
case 'electricity_subscription_number': return '#id_electricity_subscription_number';
|
||||||
representative: '#rep_national_code'
|
case 'water_meter_serial_number': return '#id_water_meter_serial_number';
|
||||||
};
|
case 'water_meter_old_serial_number': return '#id_water_meter_old_serial_number';
|
||||||
|
case 'water_meter_manufacturer': return '#id_water_meter_manufacturer';
|
||||||
|
case 'new_manufacturer': return '#id_new_manufacturer';
|
||||||
|
case 'utm_x': return '#id_utm_x';
|
||||||
|
case 'utm_y': return '#id_utm_y';
|
||||||
|
case 'utm_zone': return '#id_utm_zone';
|
||||||
|
case 'utm_hemisphere': return '#id_utm_hemisphere';
|
||||||
|
case 'well_power': return '#id_well_power';
|
||||||
|
case 'reference_letter_number': return '#id_reference_letter_number';
|
||||||
|
case 'reference_letter_date': return '#id_reference_letter_date';
|
||||||
|
case 'representative_letter_file': return '#id_representative_letter_file';
|
||||||
|
case 'representative': return '#rep_national_code';
|
||||||
|
default: return '#id_' + field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function findFieldSelector(field, context) {
|
function mapCustomerFieldToSelector(field) {
|
||||||
const $ctx = context ? $(context) : $('#requestModal');
|
switch (field) {
|
||||||
let $el = $ctx.find(`#id_${field}`).first();
|
case 'national_code': return $('#id_national_code').length ? '#id_national_code' : '#rep_national_code';
|
||||||
if ($el.length) return $el;
|
case 'first_name': return '#id_first_name';
|
||||||
$el = $ctx.find(`[name="${field}"]`).first();
|
case 'last_name': return '#id_last_name';
|
||||||
if ($el.length) return $el;
|
case 'phone_number_1': return '#id_phone_number_1';
|
||||||
$el = $ctx.find(`[data-field="${field}"]`).first();
|
case 'phone_number_2': return '#id_phone_number_2';
|
||||||
if ($el.length) return $el;
|
case 'card_number': return '#id_card_number';
|
||||||
const ex = exceptionMap[field];
|
case 'account_number': return '#id_account_number';
|
||||||
return ex ? $(ex) : $();
|
case 'bank_name': return '#id_bank_name';
|
||||||
|
case 'address': return '#id_address';
|
||||||
|
default: return '#id_' + field;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showInlineErrors(errors) {
|
function showInlineErrors(errors) {
|
||||||
if (!errors) return;
|
if (!errors) return;
|
||||||
let nonFieldWell = '';
|
let nonFieldWell = '';
|
||||||
let nonFieldCustomer = '';
|
let nonFieldCustomer = '';
|
||||||
// Request-level errors (e.g., process)
|
|
||||||
if (errors.request) {
|
|
||||||
for (const key in errors.request) {
|
|
||||||
const msgs = Array.isArray(errors.request[key]) ? errors.request[key] : [errors.request[key]];
|
|
||||||
if (key === '__all__' || key === 'non_field_errors') { continue; }
|
|
||||||
applyErrorTo(findFieldSelector(key, '#requestForm'), msgs[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errors.well) {
|
if (errors.well) {
|
||||||
for (const key in errors.well) {
|
for (const key in errors.well) {
|
||||||
const msgs = Array.isArray(errors.well[key]) ? errors.well[key] : [errors.well[key]];
|
const msgs = Array.isArray(errors.well[key]) ? errors.well[key] : [errors.well[key]];
|
||||||
if (key === '__all__' || key === 'non_field_errors') { nonFieldWell = msgs.join('، '); continue; }
|
if (key === '__all__' || key === 'non_field_errors') { nonFieldWell = msgs.join('، '); continue; }
|
||||||
applyErrorTo(findFieldSelector(key, '#wellFormBlock'), msgs[0]);
|
const sel = mapWellFieldToSelector(key);
|
||||||
|
applyErrorTo(sel, msgs[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errors.customer) {
|
if (errors.customer) {
|
||||||
for (const key in errors.customer) {
|
for (const key in errors.customer) {
|
||||||
const msgs = Array.isArray(errors.customer[key]) ? errors.customer[key] : [errors.customer[key]];
|
const msgs = Array.isArray(errors.customer[key]) ? errors.customer[key] : [errors.customer[key]];
|
||||||
if (key === '__all__' || key === 'non_field_errors') { nonFieldCustomer = msgs.join('، '); continue; }
|
if (key === '__all__' || key === 'non_field_errors') { nonFieldCustomer = msgs.join('، '); continue; }
|
||||||
applyErrorTo(findFieldSelector(key, '#repNewFields'), msgs[0]);
|
const sel = mapCustomerFieldToSelector(key);
|
||||||
|
applyErrorTo(sel, msgs[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nonFieldWell) setStatus('#wellStatus', nonFieldWell, 'danger');
|
if (nonFieldWell) setStatus('#wellStatus', nonFieldWell, 'danger');
|
||||||
|
@ -633,7 +530,7 @@
|
||||||
$('#remove-file').val('false');
|
$('#remove-file').val('false');
|
||||||
// Initialize Persian Date Picker after well form is shown
|
// Initialize Persian Date Picker after well form is shown
|
||||||
setTimeout(initPersianDatePicker, 100);
|
setTimeout(initPersianDatePicker, 100);
|
||||||
setStatus('#wellStatus', 'چاه یافت نشد. اطلاعات چاه را وارد کنید.', 'danger');
|
setStatus('#wellStatus', 'چاه یافت نشد. با ذخیره، ایجاد خواهد شد.', 'danger');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
|
.fail(function(){ setStatus('#wellStatus', 'خطا در بررسی چاه', 'danger'); });
|
||||||
|
@ -691,26 +588,46 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#btnSaveRequest').on('click', function(){
|
$('#btnSaveRequest').on('click', function(){
|
||||||
clearInlineErrors();
|
const formData = new FormData();
|
||||||
// Use form's native FormData - much cleaner!
|
formData.append('csrfmiddlewaretoken', $('input[name=csrfmiddlewaretoken]').val());
|
||||||
const formData = new FormData(document.getElementById('requestForm'));
|
formData.append('process', $('#req_process').val());
|
||||||
|
formData.append('description', $('#req_description').val());
|
||||||
// Add custom fields that aren't in the form
|
formData.append('water_subscription_number', $('#req_water_sub').val().trim());
|
||||||
if (currentWellId) formData.append('well_id', currentWellId);
|
if (currentWellId) formData.append('well_id', currentWellId);
|
||||||
if (currentRepId) formData.append('representative_id', currentRepId);
|
if (currentRepId) formData.append('representative_id', currentRepId);
|
||||||
|
// Send fields using CustomerForm names if visible
|
||||||
// Handle special national_code logic (prefer visible field)
|
const ncField = $('#id_national_code').length ? $('#id_national_code').val() : '';
|
||||||
const ncField = $('#id_national_code').val();
|
formData.append('national_code', (ncField || $('#rep_national_code').val().trim()));
|
||||||
if (ncField) {
|
formData.append('first_name', $('#id_first_name').val() || '');
|
||||||
formData.set('national_code', ncField);
|
formData.append('last_name', $('#id_last_name').val() || '');
|
||||||
} else {
|
formData.append('phone_number_1', $('#id_phone_number_1').val() || '');
|
||||||
formData.set('national_code', $('#rep_national_code').val().trim());
|
formData.append('phone_number_2', $('#id_phone_number_2').val() || '');
|
||||||
}
|
formData.append('card_number', $('#id_card_number').val() || '');
|
||||||
|
formData.append('account_number', $('#id_account_number').val() || '');
|
||||||
// Handle Persian date conversion
|
formData.append('address', $('#id_address').val() || '');
|
||||||
|
formData.append('bank_name', $('#id_bank_name').val() || '');
|
||||||
|
// Include WellForm fields so edits are saved
|
||||||
|
if ($('#wellFormBlock').is(':visible')) {
|
||||||
|
formData.append('electricity_subscription_number', $('#id_electricity_subscription_number').val() || '');
|
||||||
|
formData.append('water_meter_serial_number', $('#id_water_meter_serial_number').val() || '');
|
||||||
|
formData.append('water_meter_old_serial_number', $('#id_water_meter_old_serial_number').val() || '');
|
||||||
|
formData.append('water_meter_manufacturer', $('#id_water_meter_manufacturer').is(':visible') ? ($('#id_water_meter_manufacturer').val() || '') : '');
|
||||||
|
formData.append('new_manufacturer', $('#id_new_manufacturer').is(':visible') ? ($('#id_new_manufacturer').val() || '') : '');
|
||||||
|
formData.append('utm_x', $('#id_utm_x').val() || '');
|
||||||
|
formData.append('utm_y', $('#id_utm_y').val() || '');
|
||||||
|
formData.append('utm_zone', $('#id_utm_zone').val() || '');
|
||||||
|
formData.append('utm_hemisphere', $('#id_utm_hemisphere').val() || '');
|
||||||
|
formData.append('well_power', $('#id_well_power').val() || '');
|
||||||
|
formData.append('reference_letter_number', $('#id_reference_letter_number').val() || '');
|
||||||
|
// Use gregorian date if available, otherwise use the field value
|
||||||
const gregorianDate = $('#id_reference_letter_date').attr('data-gregorian');
|
const gregorianDate = $('#id_reference_letter_date').attr('data-gregorian');
|
||||||
if (gregorianDate) {
|
formData.append('reference_letter_date', gregorianDate || $('#id_reference_letter_date').val() || '');
|
||||||
formData.set('reference_letter_date', gregorianDate);
|
// Remove flag
|
||||||
|
formData.append('remove_file', $('#remove-file').val() || 'false');
|
||||||
|
const repFile = document.getElementById('id_representative_letter_file');
|
||||||
|
if (repFile && repFile.files && repFile.files[0]) {
|
||||||
|
formData.append('representative_letter_file', repFile.files[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const $btn = $(this).prop('disabled', true).text('در حال ذخیره...');
|
const $btn = $(this).prop('disabled', true).text('در حال ذخیره...');
|
||||||
|
@ -729,10 +646,6 @@
|
||||||
setTimeout(function(){ location.reload(); }, 1200);
|
setTimeout(function(){ location.reload(); }, 1200);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clearInlineErrors();
|
|
||||||
if (resp.errors) {
|
|
||||||
showInlineErrors(resp.errors);
|
|
||||||
}
|
|
||||||
const msg = buildErrorMessage(resp);
|
const msg = buildErrorMessage(resp);
|
||||||
showToast(msg, 'danger');
|
showToast(msg, 'danger');
|
||||||
}
|
}
|
||||||
|
@ -740,10 +653,6 @@
|
||||||
let msg = 'خطا در ذخیره';
|
let msg = 'خطا در ذخیره';
|
||||||
try {
|
try {
|
||||||
const resp = JSON.parse(xhr.responseText);
|
const resp = JSON.parse(xhr.responseText);
|
||||||
clearInlineErrors();
|
|
||||||
if (resp && resp.errors) {
|
|
||||||
showInlineErrors(resp.errors);
|
|
||||||
}
|
|
||||||
msg = buildErrorMessage(resp) || msg;
|
msg = buildErrorMessage(resp) || msg;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
showToast(msg, 'danger');
|
showToast(msg, 'danger');
|
||||||
|
@ -800,14 +709,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enforce digit-only and max length for national code input
|
|
||||||
$('#rep_national_code').on('input', function() {
|
|
||||||
const cleaned = (this.value || '').replace(/\D/g, '').slice(0, 10);
|
|
||||||
if (this.value !== cleaned) {
|
|
||||||
this.value = cleaned;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#requestModal').on('hidden.bs.modal', function(){
|
$('#requestModal').on('hidden.bs.modal', function(){
|
||||||
$('#requestForm')[0].reset();
|
$('#requestForm')[0].reset();
|
||||||
$('#wellFormBlock').hide();
|
$('#wellFormBlock').hide();
|
||||||
|
|
|
@ -26,10 +26,8 @@ def stepper_header(instance, current_step=None):
|
||||||
step_instance = next((si for si in step_instances if si.step_id == step.id), None)
|
step_instance = next((si for si in step_instances if si.step_id == step.id), None)
|
||||||
status = step_id_to_status.get(step.id, 'pending')
|
status = step_id_to_status.get(step.id, 'pending')
|
||||||
|
|
||||||
# بررسی دسترسی به مرحله (UI navigation constraints):
|
# بررسی دسترسی به مرحله
|
||||||
# can_access = instance.can_access_step(step)
|
can_access = instance.can_access_step(step)
|
||||||
# فقط مراحل تکمیلشده یا مرحله جاری قابل کلیک هستند
|
|
||||||
can_access = (step_id_to_status.get(step.id) == 'completed') or (instance.current_step and step.id == instance.current_step.id)
|
|
||||||
# مرحله انتخابشده (نمایش فعلی)
|
# مرحله انتخابشده (نمایش فعلی)
|
||||||
is_selected = bool(current_step and step.id == current_step.id)
|
is_selected = bool(current_step and step.id == current_step.id)
|
||||||
# مرحلهای که باید انجام شود (مرحله جاری در instance)
|
# مرحلهای که باید انجام شود (مرحله جاری در instance)
|
||||||
|
|
|
@ -14,6 +14,11 @@ urlpatterns = [
|
||||||
# New step-based architecture
|
# New step-based architecture
|
||||||
path('instance/<int:instance_id>/steps/', views.instance_steps, name='instance_steps'),
|
path('instance/<int:instance_id>/steps/', views.instance_steps, name='instance_steps'),
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
|
path('instance/<int:instance_id>/step/<int:step_id>/', views.step_detail, name='step_detail'),
|
||||||
path('instance/<int:instance_id>/summary/', views.instance_summary, name='instance_summary'),
|
|
||||||
|
|
||||||
|
# Legacy process views
|
||||||
|
path('', views.process_list, name='process_list'),
|
||||||
|
path('<int:process_id>/', views.process_detail, name='process_detail'),
|
||||||
|
path('<int:process_id>/start/', views.start_process, name='start_process'),
|
||||||
|
path('instance/<int:instance_id>/', views.instance_detail, name='instance_detail'),
|
||||||
|
path('my-processes/', views.my_processes, name='my_processes'),
|
||||||
]
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
import json
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
@ -10,48 +11,41 @@ from django.contrib.auth import get_user_model
|
||||||
from .models import Process, ProcessInstance, StepInstance
|
from .models import Process, ProcessInstance, StepInstance
|
||||||
from wells.models import Well
|
from wells.models import Well
|
||||||
from accounts.models import Profile
|
from accounts.models import Profile
|
||||||
|
from .forms import ProcessInstanceForm
|
||||||
from accounts.forms import CustomerForm
|
from accounts.forms import CustomerForm
|
||||||
from wells.forms import WellForm
|
from wells.forms import WellForm
|
||||||
from wells.models import WaterMeterManufacturer
|
from wells.models import WaterMeterManufacturer
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def request_list(request):
|
def process_list(request):
|
||||||
"""نمایش لیست درخواستها با جدول و مدال ایجاد"""
|
"""نمایش لیست فرآیندهای فعال"""
|
||||||
instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created')
|
|
||||||
processes = Process.objects.filter(is_active=True)
|
processes = Process.objects.filter(is_active=True)
|
||||||
manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
|
return render(request, 'processes/process_list.html', {
|
||||||
|
'processes': processes
|
||||||
# Calculate progress for each instance
|
|
||||||
instances_with_progress = []
|
|
||||||
for instance in instances:
|
|
||||||
total_steps = instance.process.steps.count()
|
|
||||||
completed_steps = instance.step_instances.filter(status='completed').count()
|
|
||||||
progress_percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
|
|
||||||
|
|
||||||
instances_with_progress.append({
|
|
||||||
'instance': instance,
|
|
||||||
'progress_percentage': round(progress_percentage),
|
|
||||||
'completed_steps': completed_steps,
|
|
||||||
'total_steps': total_steps,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Summary stats for header cards
|
@login_required
|
||||||
total_count = instances.count()
|
def process_detail(request, process_id):
|
||||||
completed_count = instances.filter(status='completed').count()
|
"""نمایش جزئیات فرآیند"""
|
||||||
in_progress_count = instances.filter(status='in_progress').count()
|
process = get_object_or_404(Process, id=process_id, is_active=True)
|
||||||
pending_count = instances.filter(status='pending').count()
|
return render(request, 'processes/process_detail.html', {
|
||||||
|
'process': process
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def request_list(request):
|
||||||
|
"""نمایش لیست درخواستها با جدول و مدال ایجاد"""
|
||||||
|
instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').filter(is_deleted=False).order_by('-created')
|
||||||
|
processes = Process.objects.filter(is_active=True)
|
||||||
|
manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
|
||||||
return render(request, 'processes/request_list.html', {
|
return render(request, 'processes/request_list.html', {
|
||||||
'instances_with_progress': instances_with_progress,
|
'instances': instances,
|
||||||
'customer_form': CustomerForm(),
|
'customer_form': CustomerForm(),
|
||||||
'well_form': WellForm(),
|
'well_form': WellForm(),
|
||||||
'processes': processes,
|
'processes': processes,
|
||||||
'manufacturers': manufacturers,
|
'manufacturers': manufacturers
|
||||||
'total_count': total_count,
|
|
||||||
'completed_count': completed_count,
|
|
||||||
'in_progress_count': in_progress_count,
|
|
||||||
'pending_count': pending_count,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,12 +127,23 @@ def create_request_with_entities(request):
|
||||||
well_id = request.POST.get('well_id') # optional if existing
|
well_id = request.POST.get('well_id') # optional if existing
|
||||||
# Representative fields
|
# Representative fields
|
||||||
representative_id = request.POST.get('representative_id')
|
representative_id = request.POST.get('representative_id')
|
||||||
|
# Prefer plain CustomerForm keys; fallback to representative_* keys
|
||||||
|
representative_national_code = request.POST.get('national_code') or request.POST.get('representative_national_code')
|
||||||
|
representative_first_name = request.POST.get('first_name') or request.POST.get('representative_first_name')
|
||||||
|
representative_last_name = request.POST.get('last_name') or request.POST.get('representative_last_name')
|
||||||
|
representative_username = request.POST.get('username') or request.POST.get('representative_username')
|
||||||
|
representative_phone_number_1 = request.POST.get('phone_number_1') or request.POST.get('representative_phone_number_1')
|
||||||
|
representative_phone_number_2 = request.POST.get('phone_number_2') or request.POST.get('representative_phone_number_2')
|
||||||
|
representative_card_number = request.POST.get('card_number') or request.POST.get('representative_card_number')
|
||||||
|
representative_account_number = request.POST.get('account_number') or request.POST.get('representative_account_number')
|
||||||
|
representative_bank_name = request.POST.get('bank_name') or request.POST.get('representative_bank_name')
|
||||||
|
representative_address = request.POST.get('address') or request.POST.get('representative_address')
|
||||||
|
|
||||||
if not process_id:
|
if not process_id:
|
||||||
return JsonResponse({'ok': False, 'errors': {'request': {'process': ['فرآیند الزامی است']}}}, status=400)
|
return JsonResponse({'ok': False, 'errors': {'request': {'process': ['فرآیند الزامی است']}}}, status=400)
|
||||||
if not water_subscription_number:
|
if not water_subscription_number:
|
||||||
return JsonResponse({'ok': False, 'errors': {'well': {'water_subscription_number': ['شماره اشتراک آب الزامی است']}}}, status=400)
|
return JsonResponse({'ok': False, 'errors': {'well': {'water_subscription_number': ['شماره اشتراک آب الزامی است']}}}, status=400)
|
||||||
if not representative_id and not request.POST.get('national_code'):
|
if not representative_id and not representative_national_code:
|
||||||
return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400)
|
return JsonResponse({'ok': False, 'errors': {'customer': {'national_code': ['کد ملی نماینده را وارد کنید یا دکمه بررسی/افزودن نماینده را بزنید']}}}, status=400)
|
||||||
|
|
||||||
representative_user = None
|
representative_user = None
|
||||||
|
@ -147,20 +152,52 @@ def create_request_with_entities(request):
|
||||||
representative_profile = Profile.objects.select_related('user').filter(user_id=representative_id).first()
|
representative_profile = Profile.objects.select_related('user').filter(user_id=representative_id).first()
|
||||||
if not representative_profile:
|
if not representative_profile:
|
||||||
return JsonResponse({'ok': False, 'errors': {'customer': {'__all__': ['نماینده انتخابشده یافت نشد']}}}, status=400)
|
return JsonResponse({'ok': False, 'errors': {'customer': {'__all__': ['نماینده انتخابشده یافت نشد']}}}, status=400)
|
||||||
# Use CustomerForm with request.POST data, merging with existing values
|
|
||||||
customer_form = CustomerForm(request.POST, instance=representative_profile)
|
|
||||||
customer_form.request = request
|
|
||||||
if not customer_form.is_valid():
|
|
||||||
return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
|
|
||||||
representative_profile = customer_form.save()
|
|
||||||
representative_user = representative_profile.user
|
representative_user = representative_profile.user
|
||||||
|
# Optionally update if fields provided
|
||||||
|
changed = False
|
||||||
|
if representative_first_name:
|
||||||
|
representative_user.first_name = representative_first_name
|
||||||
|
changed = True
|
||||||
|
if representative_last_name:
|
||||||
|
representative_user.last_name = representative_last_name
|
||||||
|
changed = True
|
||||||
|
if representative_username:
|
||||||
|
representative_user.username = representative_username
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
representative_user.save()
|
||||||
|
if representative_national_code:
|
||||||
|
representative_profile.national_code = representative_national_code
|
||||||
|
if representative_phone_number_1 is not None:
|
||||||
|
representative_profile.phone_number_1 = representative_phone_number_1
|
||||||
|
if representative_phone_number_2 is not None:
|
||||||
|
representative_profile.phone_number_2 = representative_phone_number_2
|
||||||
|
if representative_card_number is not None:
|
||||||
|
representative_profile.card_number = representative_card_number
|
||||||
|
if representative_account_number is not None:
|
||||||
|
representative_profile.account_number = representative_account_number
|
||||||
|
if representative_bank_name is not None:
|
||||||
|
representative_profile.bank_name = representative_bank_name
|
||||||
|
if representative_address is not None:
|
||||||
|
representative_profile.address = representative_address
|
||||||
|
representative_profile.save()
|
||||||
else:
|
else:
|
||||||
# Use CustomerForm to validate/create/update representative profile by national code
|
# Use CustomerForm to validate/create/update representative profile by national code
|
||||||
profile_instance = None
|
profile_instance = None
|
||||||
national_code = request.POST.get('national_code')
|
if representative_national_code:
|
||||||
if national_code:
|
profile_instance = Profile.objects.filter(national_code=representative_national_code).first()
|
||||||
profile_instance = Profile.objects.filter(national_code=national_code).first()
|
customer_data = {
|
||||||
customer_form = CustomerForm(request.POST, instance=profile_instance)
|
'first_name': representative_first_name or '',
|
||||||
|
'last_name': representative_last_name or '',
|
||||||
|
'phone_number_1': representative_phone_number_1 or '',
|
||||||
|
'phone_number_2': representative_phone_number_2 or '',
|
||||||
|
'national_code': representative_national_code or '',
|
||||||
|
'address': representative_address or '',
|
||||||
|
'card_number': representative_card_number or '',
|
||||||
|
'account_number': representative_account_number or '',
|
||||||
|
'bank_name': representative_bank_name or '',
|
||||||
|
}
|
||||||
|
customer_form = CustomerForm(customer_data, instance=profile_instance)
|
||||||
customer_form.request = request
|
customer_form.request = request
|
||||||
if not customer_form.is_valid():
|
if not customer_form.is_valid():
|
||||||
return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
|
return JsonResponse({'ok': False, 'errors': {'customer': customer_form.errors}}, status=400)
|
||||||
|
@ -255,24 +292,62 @@ def create_request_with_entities(request):
|
||||||
redirect_url = reverse('processes:instance_steps', args=[instance.id])
|
redirect_url = reverse('processes:instance_steps', args=[instance.id])
|
||||||
return JsonResponse({'ok': True, 'instance_id': instance.id, 'redirect': redirect_url})
|
return JsonResponse({'ok': True, 'instance_id': instance.id, 'redirect': redirect_url})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def delete_request(request, instance_id):
|
def delete_request(request, instance_id):
|
||||||
"""حذف درخواست"""
|
"""حذف درخواست"""
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
code = instance.code
|
code = instance.code
|
||||||
if instance.status == 'completed':
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'message': 'درخواست تکمیل شده نمیتواند حذف شود'
|
|
||||||
})
|
|
||||||
instance.delete()
|
instance.delete()
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'درخواست {code} با موفقیت حذف شد'
|
'message': f'درخواست {code} با موفقیت حذف شد'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def start_process(request, process_id):
|
||||||
|
"""شروع فرآیند جدید"""
|
||||||
|
process = get_object_or_404(Process, id=process_id, is_active=True)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ProcessInstanceForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
instance = form.save(commit=False)
|
||||||
|
instance.process = process
|
||||||
|
instance.requester = request.user
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
# ایجاد نمونههای مرحله
|
||||||
|
for step in process.steps.all():
|
||||||
|
StepInstance.objects.create(
|
||||||
|
process_instance=instance,
|
||||||
|
step=step
|
||||||
|
)
|
||||||
|
|
||||||
|
# تنظیم مرحله اول به عنوان مرحله فعلی
|
||||||
|
first_step = process.steps.first()
|
||||||
|
if first_step:
|
||||||
|
instance.current_step = first_step
|
||||||
|
instance.status = 'in_progress'
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
messages.success(request, f'فرآیند {process.name} با موفقیت شروع شد.')
|
||||||
|
return redirect('processes:instance_detail', instance_id=instance.id)
|
||||||
|
else:
|
||||||
|
form = ProcessInstanceForm()
|
||||||
|
|
||||||
|
return render(request, 'processes/start_process.html', {
|
||||||
|
'process': process,
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def instance_detail(request, instance_id):
|
||||||
|
"""نمایش جزئیات نمونه فرآیند"""
|
||||||
|
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
||||||
|
return render(request, 'processes/instance_detail.html', {
|
||||||
|
'instance': instance
|
||||||
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def step_detail(request, instance_id, step_id):
|
def step_detail(request, instance_id, step_id):
|
||||||
|
@ -282,17 +357,6 @@ def step_detail(request, instance_id, step_id):
|
||||||
id=instance_id
|
id=instance_id
|
||||||
)
|
)
|
||||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
step = get_object_or_404(instance.process.steps, id=step_id)
|
||||||
# If the request is already completed, redirect to read-only summary page
|
|
||||||
if instance.status == 'completed':
|
|
||||||
return redirect('processes:instance_summary', instance_id=instance.id)
|
|
||||||
|
|
||||||
# جلوگیری از پرش به مراحل آینده: فقط اجازه نمایش مرحله جاری یا مراحل تکمیلشده
|
|
||||||
try:
|
|
||||||
if instance.current_step and step.order > instance.current_step.order:
|
|
||||||
messages.error(request, 'ابتدا مراحل قبلی را تکمیل کنید.')
|
|
||||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# بررسی دسترسی به مرحله
|
# بررسی دسترسی به مرحله
|
||||||
if not instance.can_access_step(step):
|
if not instance.can_access_step(step):
|
||||||
|
@ -334,7 +398,6 @@ def step_detail(request, instance_id, step_id):
|
||||||
'next_step': next_step,
|
'next_step': next_step,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def instance_steps(request, instance_id):
|
def instance_steps(request, instance_id):
|
||||||
"""هدایت به مرحله فعلی instance"""
|
"""هدایت به مرحله فعلی instance"""
|
||||||
|
@ -351,41 +414,16 @@ def instance_steps(request, instance_id):
|
||||||
messages.error(request, 'هیچ مرحلهای برای این فرآیند تعریف نشده است.')
|
messages.error(request, 'هیچ مرحلهای برای این فرآیند تعریف نشده است.')
|
||||||
return redirect('processes:request_list')
|
return redirect('processes:request_list')
|
||||||
|
|
||||||
# If completed, go to summary instead of steps
|
|
||||||
if instance.status == 'completed':
|
|
||||||
return redirect('processes:instance_summary', instance_id=instance.id)
|
|
||||||
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
return redirect('processes:step_detail', instance_id=instance.id, step_id=instance.current_step.id)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def instance_summary(request, instance_id):
|
def my_processes(request):
|
||||||
"""نمای خلاصهٔ فقطخواندنی برای درخواستهای تکمیلشده."""
|
"""نمایش فرآیندهای کاربر"""
|
||||||
instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id)
|
my_instances = ProcessInstance.objects.filter(requester=request.user)
|
||||||
# Only show for completed requests; otherwise route to steps
|
assigned_steps = StepInstance.objects.filter(assigned_to=request.user, status='in_progress')
|
||||||
if instance.status != 'completed':
|
|
||||||
return redirect('processes:instance_steps', instance_id=instance.id)
|
|
||||||
|
|
||||||
# Collect final invoice, payments, and certificate if any
|
return render(request, 'processes/my_processes.html', {
|
||||||
from invoices.models import Invoice
|
'my_instances': my_instances,
|
||||||
from installations.models import InstallationReport
|
'assigned_steps': assigned_steps
|
||||||
from certificates.models import CertificateInstance
|
|
||||||
invoice = Invoice.objects.filter(process_instance=instance).first()
|
|
||||||
payments = invoice.payments.filter(is_deleted=False).all() if invoice else []
|
|
||||||
latest_report = InstallationReport.objects.filter(assignment__process_instance=instance).order_by('-created').first()
|
|
||||||
certificate = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
|
|
||||||
|
|
||||||
# Build rows like final invoice step
|
|
||||||
rows = []
|
|
||||||
if invoice:
|
|
||||||
items_qs = invoice.items.select_related('item').filter(is_deleted=False).all()
|
|
||||||
rows = list(items_qs)
|
|
||||||
|
|
||||||
return render(request, 'processes/instance_summary.html', {
|
|
||||||
'instance': instance,
|
|
||||||
'invoice': invoice,
|
|
||||||
'payments': payments,
|
|
||||||
'rows': rows,
|
|
||||||
'latest_report': latest_report,
|
|
||||||
'certificate': certificate,
|
|
||||||
})
|
})
|
||||||
|
|
29202
static/assets/vendor/css/rtl/core-dark.css
vendored
29202
static/assets/vendor/css/rtl/core-dark.css
vendored
File diff suppressed because one or more lines are too long
|
@ -35,7 +35,7 @@ id="layout-navbar">
|
||||||
<!-- /Language -->
|
<!-- /Language -->
|
||||||
|
|
||||||
<!-- Quick links -->
|
<!-- Quick links -->
|
||||||
<li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0 d-none">
|
<li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0">
|
||||||
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
|
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
|
||||||
data-bs-auto-close="outside" aria-expanded="false">
|
data-bs-auto-close="outside" aria-expanded="false">
|
||||||
<i class='bx bx-grid-alt bx-sm'></i>
|
<i class='bx bx-grid-alt bx-sm'></i>
|
||||||
|
@ -127,17 +127,17 @@ id="layout-navbar">
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-styles">
|
<ul class="dropdown-menu dropdown-menu-end dropdown-styles">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#" data-theme="light">
|
<a class="dropdown-item" href="#" data-theme="light">
|
||||||
<span class="align-middle"><i class='bx bx-sun me-2'></i>روشن</span>
|
<span class="align-middle"><i class='bx bx-sun me-2'></i>Light</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#" data-theme="dark">
|
<a class="dropdown-item" href="#" data-theme="dark">
|
||||||
<span class="align-middle"><i class="bx bx-moon me-2"></i>تاریک</span>
|
<span class="align-middle"><i class="bx bx-moon me-2"></i>Dark</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#" data-theme="system">
|
<a class="dropdown-item" href="#" data-theme="system">
|
||||||
<span class="align-middle"><i class="bx bx-desktop me-2"></i>سیستم</span>
|
<span class="align-middle"><i class="bx bx-desktop me-2"></i>System</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -146,7 +146,7 @@ id="layout-navbar">
|
||||||
|
|
||||||
|
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1 d-none">
|
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-1">
|
||||||
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
|
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown"
|
||||||
data-bs-auto-close="outside" aria-expanded="false">
|
data-bs-auto-close="outside" aria-expanded="false">
|
||||||
<i class="bx bx-bell bx-sm"></i>
|
<i class="bx bx-bell bx-sm"></i>
|
||||||
|
@ -306,6 +306,24 @@ id="layout-navbar">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<span class="avatar-initial rounded-circle bg-label-warning"><i class="bx bx-error"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">CPU is running high</h6>
|
||||||
|
<p class="mb-0">CPU Utilization Percent is currently at 88.63%,</p>
|
||||||
|
<small class="text-muted">5 days ago</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 dropdown-notifications-actions">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-read"><span class="badge badge-dot"></span></a>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-archive"><span class="bx bx-x"></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown-menu-footer border-top p-3">
|
<li class="dropdown-menu-footer border-top p-3">
|
||||||
|
@ -314,36 +332,25 @@ id="layout-navbar">
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<!--/ Notification -->
|
<!--/ Notification -->
|
||||||
|
|
||||||
<!-- User -->
|
<!-- User -->
|
||||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||||
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle hide-arrow" href="#" data-bs-toggle="dropdown">
|
||||||
<div class="avatar avatar-online">
|
<div class="avatar avatar-online">
|
||||||
{% if request.user.is_authenticated and request.user.profile and request.user.profile.pic %}
|
|
||||||
<img src="{{ request.user.profile.pic.url }}" alt class="w-px-40 h-auto rounded-circle">
|
|
||||||
{% else %}
|
|
||||||
<img src="{% static 'assets/img/avatars/1.png' %}" alt class="w-px-40 h-auto rounded-circle">
|
<img src="{% static 'assets/img/avatars/1.png' %}" alt class="w-px-40 h-auto rounded-circle">
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#">
|
<a class="dropdown-item" href="pages-account-settings-account.html">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="flex-shrink-0 me-3">
|
<div class="flex-shrink-0 me-3">
|
||||||
<div class="avatar avatar-online">
|
<div class="avatar avatar-online">
|
||||||
{% if request.user.is_authenticated and request.user.profile and request.user.profile.pic %}
|
|
||||||
<img src="{{ request.user.profile.pic.url }}" alt class="w-px-40 h-auto rounded-circle">
|
|
||||||
{% else %}
|
|
||||||
<img src="{% static 'assets/img/avatars/1.png' %}" alt class="w-px-40 h-auto rounded-circle">
|
<img src="{% static 'assets/img/avatars/1.png' %}" alt class="w-px-40 h-auto rounded-circle">
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<span class="fw-medium d-block">{{ request.user.get_full_name|default:request.user.username }}</span>
|
<span class="fw-medium d-block">John Doe</span>
|
||||||
{% if request.user.profile %}
|
<small class="text-muted">Admin</small>
|
||||||
<small class="text-muted">{{ request.user.profile.roles_str }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -352,24 +359,48 @@ id="layout-navbar">
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#">
|
<a class="dropdown-item" href="pages-profile-user.html">
|
||||||
<i class="bx bx-user me-2"></i>
|
<i class="bx bx-user me-2"></i>
|
||||||
<span class="align-middle">پروفایل</span>
|
<span class="align-middle">My Profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#">
|
<a class="dropdown-item" href="pages-account-settings-account.html">
|
||||||
<i class="bx bx-cog me-2"></i>
|
<i class="bx bx-cog me-2"></i>
|
||||||
<span class="align-middle">تنظیمات</span>
|
<span class="align-middle">Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-account-settings-billing.html">
|
||||||
|
<span class="d-flex align-items-center align-middle">
|
||||||
|
<i class="flex-shrink-0 bx bx-credit-card me-2"></i>
|
||||||
|
<span class="flex-grow-1 align-middle">Billing</span>
|
||||||
|
<span class="flex-shrink-0 badge badge-center rounded-pill bg-danger w-px-20 h-px-20">4</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'accounts:logout' %}">
|
<a class="dropdown-item" href="pages-faq.html">
|
||||||
|
<i class="bx bx-help-circle me-2"></i>
|
||||||
|
<span class="align-middle">FAQ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-pricing.html">
|
||||||
|
<i class="bx bx-dollar me-2"></i>
|
||||||
|
<span class="align-middle">Pricing</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="auth-login-cover.html" target="_blank">
|
||||||
<i class="bx bx-power-off me-2"></i>
|
<i class="bx bx-power-off me-2"></i>
|
||||||
<span class="align-middle">خروج</span>
|
<span class="align-middle">Log Out</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<ul class="menu-inner py-1">
|
<ul class="menu-inner py-1">
|
||||||
<!-- Dashboards -->
|
<!-- Dashboards -->
|
||||||
<li class="menu-item d-none">
|
<li class="menu-item">
|
||||||
<a href="#" class="menu-link menu-toggle">
|
<a href="#" class="menu-link menu-toggle">
|
||||||
<i class="menu-icon tf-icons bx bx-home-circle"></i>
|
<i class="menu-icon tf-icons bx bx-home-circle"></i>
|
||||||
<div class="text-truncate">داشبورد</div>
|
<div class="text-truncate">داشبورد</div>
|
||||||
|
|
|
@ -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 django.db.models.deletion
|
||||||
import simple_history.models
|
import simple_history.models
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue