Compare commits
7 commits
bf4047714c
...
80fcdca876
Author | SHA1 | Date | |
---|---|---|---|
80fcdca876 | |||
9124e5d52c | |||
6cecb7fa80 | |||
3acbeb7770 | |||
204b0aa48e | |||
af40e169ae | |||
246a2c0759 |
29 changed files with 1337 additions and 452 deletions
|
@ -33,9 +33,9 @@ class ProfileAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
class CompanyAdmin(admin.ModelAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'logo', 'signature', 'address', 'phone']
|
list_display = ['name', 'logo', 'signature', 'address', 'phone', 'broker', 'registration_number']
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
search_fields = ['name', 'address', 'phone']
|
search_fields = ['name', 'address', 'phone']
|
||||||
list_filter = ['is_active']
|
list_filter = ['is_active', 'broker']
|
||||||
date_hierarchy = 'created'
|
date_hierarchy = 'created'
|
||||||
ordering = ['-created']
|
ordering = ['-created']
|
20
accounts/migrations/0002_company_broker.py
Normal file
20
accounts/migrations/0002_company_broker.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 13:43
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
('locations', '0003_remove_broker_company'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='broker',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='company', to='locations.broker', verbose_name='کارگزار'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 14:11
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_company_broker'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='account_number',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='bank_name',
|
||||||
|
field=models.CharField(blank=True, choices=[('mellat', 'بانک ملت'), ('saman', 'بانک سامان'), ('parsian', 'بانک پارسیان'), ('sina', 'بانک سینا'), ('tejarat', 'بانک تجارت'), ('tosee', 'بانک توسعه'), ('iran_zamin', 'بانک ایران زمین'), ('meli', 'بانک ملی'), ('saderat', 'بانک توسعه صادرات'), ('iran_zamin', 'بانک ایران زمین'), ('refah', 'بانک رفاه'), ('eghtesad_novin', 'بانک اقتصاد نوین'), ('pasargad', 'بانک پاسارگاد'), ('other', 'سایر')], max_length=255, null=True, verbose_name='نام بانک'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='card_number',
|
||||||
|
field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='sheba_number',
|
||||||
|
field=models.CharField(blank=True, max_length=30, null=True, verbose_name='شماره شبا'),
|
||||||
|
),
|
||||||
|
]
|
18
accounts/migrations/0004_company_branch_name.py
Normal file
18
accounts/migrations/0004_company_branch_name.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 14:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0003_company_account_number_company_bank_name_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='branch_name',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شعبه بانک'),
|
||||||
|
),
|
||||||
|
]
|
18
accounts/migrations/0005_company_registration_number.py
Normal file
18
accounts/migrations/0005_company_registration_number.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-08 10:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0004_company_branch_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='registration_number',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='شماره ثبت شرکت'),
|
||||||
|
),
|
||||||
|
]
|
18
accounts/migrations/0006_company_card_holder_name.py
Normal file
18
accounts/migrations/0006_company_card_holder_name.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-08 10:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0005_company_registration_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='company',
|
||||||
|
name='card_holder_name',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='نام دارنده کارت'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -181,11 +181,94 @@ class Profile(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Company(NameSlugModel):
|
class Company(NameSlugModel):
|
||||||
logo = models.ImageField(upload_to='companies/logos', null=True, blank=True, verbose_name='لوگوی شرکت')
|
logo = models.ImageField(
|
||||||
signature = models.ImageField(upload_to='companies/signatures', null=True, blank=True, verbose_name='امضای شرکت')
|
upload_to='companies/logos',
|
||||||
address = models.TextField(null=True, blank=True, verbose_name='آدرس')
|
null=True,
|
||||||
phone = models.CharField(max_length=11, null=True, blank=True, verbose_name='شماره تماس')
|
blank=True,
|
||||||
|
verbose_name='لوگوی شرکت'
|
||||||
|
)
|
||||||
|
signature = models.ImageField(
|
||||||
|
upload_to='companies/signatures',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='امضای شرکت'
|
||||||
|
)
|
||||||
|
address = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='آدرس'
|
||||||
|
)
|
||||||
|
phone = models.CharField(
|
||||||
|
max_length=11,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='شماره تماس'
|
||||||
|
)
|
||||||
|
registration_number = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='شماره ثبت شرکت'
|
||||||
|
)
|
||||||
|
broker = models.OneToOneField(
|
||||||
|
Broker,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name="کارگزار",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='company'
|
||||||
|
)
|
||||||
|
card_number = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
null=True,
|
||||||
|
verbose_name="شماره کارت",
|
||||||
|
blank=True,
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex=r'^\d+$',
|
||||||
|
message='شماره کارت باید فقط شامل اعداد باشد.',
|
||||||
|
code='invalid_card_number'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
account_number = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
verbose_name="شماره حساب",
|
||||||
|
blank=True,
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex=r'^\d+$',
|
||||||
|
message='شماره حساب باید فقط شامل اعداد باشد.',
|
||||||
|
code='invalid_account_number'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
card_holder_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="نام دارنده کارت",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
sheba_number = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
null=True,
|
||||||
|
verbose_name="شماره شبا",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
bank_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
choices=BANK_CHOICES,
|
||||||
|
null=True,
|
||||||
|
verbose_name="نام بانک",
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
branch_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="شعبه بانک",
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'شرکت'
|
verbose_name = 'شرکت'
|
||||||
verbose_name_plural = 'شرکتها'
|
verbose_name_plural = 'شرکتها'
|
||||||
|
|
|
@ -2,45 +2,71 @@
|
||||||
<html lang="fa" dir="rtl">
|
<html lang="fa" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>چاپ قرارداد {{ instance.code }}</title>
|
<title>چاپ قرارداد {{ instance.code }}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
{% load static %}
|
||||||
|
|
||||||
|
<!-- Match app fonts and theme -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500,1,600,1,700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@page { size: A4; margin: 1.2cm; }
|
@page { size: A4; margin: 1.2cm; }
|
||||||
body { font-family: 'Vazirmatn', sans-serif; }
|
.invoice-header { border-bottom: 1px solid #dee2e6; padding-bottom: 16px; margin-bottom: 24px; }
|
||||||
.logo { max-height: 80px; }
|
.brand-box { width:64px; height:64px; display:flex; align-items:center; justify-content:center; background:#eef2ff; border-radius:8px; }
|
||||||
.signature { height: 90px; border: 1px dashed #ccc; }
|
.logo { max-height: 58px; max-width: 120px; }
|
||||||
|
.contract-title { font-size: 20px; font-weight: 600; }
|
||||||
|
.small-muted { font-size: 12px; color: #6c757d; }
|
||||||
|
.signature-box { border: 1px dashed #ccc; height: 210px; display:flex; align-items:center; justify-content:center; }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function(){ setTimeout(function(){ window.print(); }, 300); });
|
window.onload = function(){
|
||||||
|
window.print();
|
||||||
|
setTimeout(function(){ window.close(); }, 200);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<!-- Header: Company and contract info -->
|
||||||
<div>
|
<div class="invoice-header">
|
||||||
<h5>{{ contract.template.company.name }}</h5>
|
<div class="small text-end text-muted mb-2">تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}</div>
|
||||||
<h5 class="mb-1">{{ contract.template.name }}</h5>
|
<h5 class="text-center mb-3">
|
||||||
<div class="text-muted small">کد درخواست: {{ instance.code }} | تاریخ: {{ contract.jcreated }}</div>
|
{% if instance.broker and instance.broker.company %}
|
||||||
</div>
|
{{ instance.broker.company.name }}
|
||||||
{% if contract.template.company.logo %}
|
{% elif template.company %}
|
||||||
<img class="logo" src="{{ contract.template.company.logo.url }}" alt="لوگو" />
|
{{ template.company.name }}
|
||||||
|
{% else %}
|
||||||
|
شرکت آب منطقهای
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<h4 class="text-center mb-3">{{ contract.template.name }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
|
||||||
|
<!-- Contract body -->
|
||||||
<div style="white-space: pre-line; line-height: 1.9;">{{ contract.rendered_body|safe }}</div>
|
<div style="white-space: pre-line; line-height: 1.9;">{{ contract.rendered_body|safe }}</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<!-- Signatures -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-6 text-center">
|
<div class="col-6 text-center">
|
||||||
<div>امضای مشترک</div>
|
<div>امضای مشترک</div>
|
||||||
<div class="signature mt-2"></div>
|
<div class="signature-box mt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 text-center">
|
<div class="col-6 text-center">
|
||||||
<div>امضای شرکت</div>
|
<div>امضای شرکت</div>
|
||||||
<div class="signature mt-2">
|
<div class="signature-box mt-2">
|
||||||
{% if contract.template.company.signature %}
|
{% if instance.broker and instance.broker.company and instance.broker.company.signature %}
|
||||||
<img src="{{ contract.template.company.signature.url }}" alt="امضای شرکت" style="max-height: 80px;" />
|
<img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="max-height: 200px;" />
|
||||||
|
{% elif contract.template.company and contract.template.company.signature %}
|
||||||
|
<img src="{{ contract.template.company.signature.url }}" alt="امضای شرکت" style="max-height: 200px;" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include '_toasts.html' %}
|
{% include '_toasts.html' %}
|
||||||
|
|
||||||
|
<!-- Instance Info Modal -->
|
||||||
|
{% instance_info_modal instance %}
|
||||||
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
|
@ -26,13 +30,18 @@
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
||||||
<small class="text-muted d-block">
|
<small class="text-muted d-block">
|
||||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
{% instance_info instance %}
|
||||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
<a href="{% url 'contracts:contract_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
|
||||||
<a href="{% url 'contracts:contract_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">پرینت</a>
|
<i class="bx bx-printer me-2"></i> پرینت
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
|
||||||
|
بازگشت
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,29 +50,32 @@
|
||||||
<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">
|
||||||
|
<div class="small text-end text-muted mb-2">تاریخ: {{ contract.jcreated_date }} | کد درخواست: {{ instance.code }}</div>
|
||||||
|
<h5 class="text-center mb-3">
|
||||||
|
{% if instance.broker and instance.broker.company %}
|
||||||
|
{{ instance.broker.company.name }}
|
||||||
|
{% elif template.company %}
|
||||||
|
{{ template.company.name }}
|
||||||
|
{% else %}
|
||||||
|
شرکت آب منطقهای
|
||||||
|
{% endif %}</h5>
|
||||||
|
<h4 class="text-center mb-3">{{ contract.template.name }}</h4>
|
||||||
{% if can_view_contract_body %}
|
{% if can_view_contract_body %}
|
||||||
{% if template.company.logo %}
|
|
||||||
<div class="text-center mb-3">
|
|
||||||
<img src="{{ template.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
|
|
||||||
<h4 class="text-muted">{{ contract.template.company.name }}</h4>
|
|
||||||
<h5 class="text-muted">{{ contract.template.name }}</h5>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="small text-muted mb-2">تاریخ: {{ contract.jcreated }}</div>
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="contract-body" style="white-space: pre-line; line-height:1.9;">{{ contract.rendered_body|safe }}</div>
|
<div class="contract-body" style="white-space: pre-line; line-height:1.9;">{{ contract.rendered_body|safe }}</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-6 text-center">
|
<div class="col-6 text-center">
|
||||||
<div>امضای مشترک</div>
|
<div>امضای مشترک</div>
|
||||||
<div style="height:90px;border:1px dashed #ccc; margin-top:10px;"></div>
|
<div style="height:210px;border:1px dashed #ccc; margin-top:10px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 text-center">
|
<div class="col-6 text-center">
|
||||||
<div>امضای شرکت</div>
|
<div>امضای شرکت</div>
|
||||||
<div style="height:90px;border:1px dashed #ccc; margin-top:10px;">
|
<div style="height:210px;border:1px dashed #ccc; margin-top:10px;">
|
||||||
{% if template.company.signature %}
|
{% if instance.broker and instance.broker.company and instance.broker.company.signature %}
|
||||||
<img src="{{ template.company.signature.url }}" alt="امضای شرکت" style="max-height:80px;">
|
<img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="max-height:200px;">
|
||||||
|
{% elif template.company and template.company.signature %}
|
||||||
|
<img src="{{ template.company.signature.url }}" alt="امضای شرکت" style="max-height:200px;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,15 +88,23 @@
|
||||||
<form method="post" class="d-flex justify-content-between mt-3">
|
<form method="post" class="d-flex justify-content-between mt-3">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% 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">
|
||||||
|
<i class="bx bx-chevron-right bx-sm me-sm-2"></i>
|
||||||
|
قبلی
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span></span>
|
<span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
{% if is_broker %}
|
{% if is_broker %}
|
||||||
<button type="submit" class="btn btn-primary">تایید و بعدی</button>
|
<button type="submit" class="btn btn-primary">تایید و بعدی
|
||||||
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
|
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
|
||||||
|
بعدی
|
||||||
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if is_broker %}
|
{% if is_broker %}
|
||||||
|
|
|
@ -2,29 +2,51 @@ from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from decimal import Decimal
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from processes.models import ProcessInstance, StepInstance
|
from processes.models import ProcessInstance, StepInstance
|
||||||
from common.consts import UserRoles
|
from common.consts import UserRoles
|
||||||
from .models import ContractTemplate, ContractInstance
|
from .models import ContractTemplate, ContractInstance
|
||||||
|
from invoices.models import Invoice, Quote
|
||||||
from _helpers.utils import jalali_converter2
|
from _helpers.utils import jalali_converter2
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
|
||||||
def build_contract_context(instance: ProcessInstance) -> dict:
|
def build_contract_context(instance: ProcessInstance) -> dict:
|
||||||
representative = instance.representative
|
representative = instance.representative
|
||||||
profile = getattr(representative, 'profile', None)
|
profile = getattr(representative, 'profile', None)
|
||||||
well = instance.well
|
well = instance.well
|
||||||
|
# Compute prepayment from Quote-linked invoice payments
|
||||||
|
quote = Quote.objects.filter(process_instance=instance).first()
|
||||||
|
invoice = Invoice.objects.filter(quote=quote).first() if quote else None
|
||||||
|
payments_qs = invoice.payments.filter(is_deleted=False, direction='in').all() if invoice else []
|
||||||
|
total_paid = sum((p.amount for p in payments_qs), Decimal('0'))
|
||||||
|
try:
|
||||||
|
latest_payment_date = max((p.payment_date for p in payments_qs)) if payments_qs else None
|
||||||
|
except Exception:
|
||||||
|
latest_payment_date = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'customer_full_name': representative.get_full_name() if representative else '',
|
'customer_full_name': mark_safe(f"<span class=\"fw-bold\">{representative.get_full_name() if representative else ''}</span>"),
|
||||||
'national_code': profile.national_code if profile else '',
|
'registration_number': mark_safe(f"<span class=\"fw-bold\">{instance.broker.company.registration_number if instance.broker and instance.broker.company else ''}</span>"),
|
||||||
'address': profile.address if profile else '',
|
'national_code': mark_safe(f"<span class=\"fw-bold\">{profile.national_code if profile else ''}</span>"),
|
||||||
'phone': profile.phone_number_1 if profile else '',
|
'address': mark_safe(f"<span class=\"fw-bold\">{profile.address if profile else ''}</span>"),
|
||||||
'phone2': profile.phone_number_2 if profile else '',
|
'phone': mark_safe(f"<span class=\"fw-bold\">{profile.phone_number_1 if profile else ''}</span>"),
|
||||||
'water_subscription_number': well.water_subscription_number if well else '',
|
'phone2': mark_safe(f"<span class=\"fw-bold\">{profile.phone_number_2 if profile else ''}</span>"),
|
||||||
'electricity_subscription_number': well.electricity_subscription_number if well else '',
|
'water_subscription_number': mark_safe(f"<span class=\"fw-bold\">{well.water_subscription_number if well else ''}</span>"),
|
||||||
'water_meter_serial_number': well.water_meter_serial_number if well else '',
|
'electricity_subscription_number': mark_safe(f"<span class=\"fw-bold\">{well.electricity_subscription_number if well else ''}</span>"),
|
||||||
'well_power': well.well_power if well else '',
|
'water_meter_serial_number': mark_safe(f"<span class=\"fw-bold\">{well.water_meter_serial_number if well else ''}</span>"),
|
||||||
'request_code': instance.code,
|
'well_power': mark_safe(f"<span class=\"fw-bold\">{well.well_power if well else ''}</span>"),
|
||||||
'today': jalali_converter2(timezone.now()),
|
'request_code': mark_safe(f"<span class=\"fw-bold\">{instance.code}</span>"),
|
||||||
|
'today': mark_safe(f"<span class=\"fw-bold\">{jalali_converter2(timezone.now())}</span>"),
|
||||||
|
'company_name': mark_safe(f"<span class=\"fw-bold\">{instance.broker.company.name if instance.broker and instance.broker.company else ''}</span>"),
|
||||||
|
'city_name': mark_safe(f"<span class=\"fw-bold\">{instance.broker.affairs.county.city.name if instance.broker and instance.broker.affairs and instance.broker.affairs.county and instance.broker.affairs.county.city else ''}</span>"),
|
||||||
|
'card_number': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.card_number if instance.representative else ''}</span>"),
|
||||||
|
'account_number': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.account_number if instance.representative else ''}</span>"),
|
||||||
|
'bank_name': mark_safe(f"<span class=\"fw-bold\">{instance.representative.profile.get_bank_name_display() if instance.representative else ''}</span>"),
|
||||||
|
'prepayment_amount': mark_safe(f"<span class=\"fw-bold\">{int(total_paid):,}</span>"),
|
||||||
|
'prepayment_date': mark_safe(f"<span class=\"fw-bold\">{jalali_converter2(latest_payment_date)}</span>") if latest_payment_date else '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,10 +57,7 @@ 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)
|
profile = getattr(request.user, 'profile', None)
|
||||||
is_broker = False
|
is_broker = False
|
||||||
can_view_contract_body = True
|
can_view_contract_body = True
|
||||||
|
@ -72,7 +91,6 @@ def contract_step(request, instance_id, step_id):
|
||||||
# If user submits to go next, only broker can complete and go to next
|
# If user submits to go next, only broker can complete and go to next
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if not is_broker:
|
if not is_broker:
|
||||||
from django.http import JsonResponse
|
|
||||||
return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
|
return JsonResponse({'success': False, 'message': 'شما مجوز تایید این مرحله را ندارید'}, status=403)
|
||||||
StepInstance.objects.update_or_create(
|
StepInstance.objects.update_or_create(
|
||||||
process_instance=instance,
|
process_instance=instance,
|
||||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -7,6 +7,7 @@ from decimal import Decimal
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from _helpers.utils import jalali_converter2
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -91,6 +92,7 @@ class Quote(NameSlugModel):
|
||||||
verbose_name="ایجاد کننده",
|
verbose_name="ایجاد کننده",
|
||||||
related_name='created_quotes'
|
related_name='created_quotes'
|
||||||
)
|
)
|
||||||
|
|
||||||
history = HistoricalRecords()
|
history = HistoricalRecords()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -371,3 +373,6 @@ class Payment(BaseModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def jpayment_date(self):
|
||||||
|
return jalali_converter2(self.payment_date)
|
||||||
|
|
|
@ -150,7 +150,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.jpayment_date }}</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>
|
||||||
|
@ -316,11 +316,32 @@
|
||||||
(function initPersianDatePicker(){
|
(function initPersianDatePicker(){
|
||||||
if (window.$ && $.fn.persianDatepicker && $('#id_payment_date').length) {
|
if (window.$ && $.fn.persianDatepicker && $('#id_payment_date').length) {
|
||||||
$('#id_payment_date').persianDatepicker({
|
$('#id_payment_date').persianDatepicker({
|
||||||
format: 'YYYY/MM/DD', initialValue: false, autoClose: true, persianDigit: false, observer: true,
|
format: 'YYYY/MM/DD',
|
||||||
|
initialValue: false,
|
||||||
|
autoClose: true,
|
||||||
|
persianDigit: false,
|
||||||
|
observer: true,
|
||||||
calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
|
calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
|
||||||
onSelect: function(unix){
|
onSelect: function(unix){
|
||||||
const g = new window.persianDate(unix).toCalendar('gregorian').format('YYYY-MM-DD');
|
// تبدیل تاریخ شمسی به میلادی برای ارسال به سرور
|
||||||
$('#id_payment_date').attr('data-gregorian', g);
|
const gregorianDate = new Date(unix);
|
||||||
|
const year = gregorianDate.getFullYear();
|
||||||
|
const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(gregorianDate.getDate()).padStart(2, '0');
|
||||||
|
const gregorianDateString = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// نمایش تاریخ شمسی در فیلد
|
||||||
|
if (window.persianDate) {
|
||||||
|
const persianDate = new window.persianDate(unix);
|
||||||
|
const persianDateString = persianDate.format('YYYY/MM/DD');
|
||||||
|
$('#id_payment_date').val(persianDateString);
|
||||||
|
} else {
|
||||||
|
// اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
|
||||||
|
$('#id_payment_date').val(gregorianDateString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
|
||||||
|
$('#id_payment_date').attr('data-gregorian', gregorianDateString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -328,8 +349,14 @@
|
||||||
|
|
||||||
function buildForm(){
|
function buildForm(){
|
||||||
const fd = new FormData(document.getElementById('formFinalPayment'));
|
const fd = new FormData(document.getElementById('formFinalPayment'));
|
||||||
const g = document.getElementById('id_payment_date').getAttribute('data-gregorian');
|
|
||||||
if (g) { fd.set('payment_date', g); }
|
// تبدیل تاریخ شمسی به میلادی برای ارسال
|
||||||
|
const persianDateValue = $('#id_payment_date').val();
|
||||||
|
const gregorianDateValue = $('#id_payment_date').attr('data-gregorian');
|
||||||
|
if (persianDateValue && gregorianDateValue) {
|
||||||
|
fd.set('payment_date', gregorianDateValue);
|
||||||
|
}
|
||||||
|
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
(function(){
|
(function(){
|
||||||
|
|
|
@ -18,15 +18,16 @@
|
||||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
|
<link rel="stylesheet" href="{% static 'assets/vendor/libs/bs-stepper/bs-stepper.css' %}">
|
||||||
<!-- 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>
|
|
||||||
@media print {
|
|
||||||
.no-print { display: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include '_toasts.html' %}
|
{% include '_toasts.html' %}
|
||||||
|
|
||||||
|
<!-- Instance Info Modal -->
|
||||||
|
{% instance_info_modal instance %}
|
||||||
|
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -35,19 +36,21 @@
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
||||||
<small class="text-muted d-block">
|
<small class="text-muted d-block">
|
||||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
{% instance_info instance %}
|
||||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
|
<a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
|
||||||
<i class="bx bx-printer"></i> پرینت
|
<i class="bx bx-printer me-2"></i> پرینت
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
|
||||||
|
بازگشت
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bs-stepper wizard-vertical vertical mt-2 no-print">
|
<div class="bs-stepper wizard-vertical vertical mt-2">
|
||||||
{% stepper_header instance step %}
|
{% stepper_header instance step %}
|
||||||
<div class="bs-stepper-content">
|
<div class="bs-stepper-content">
|
||||||
|
|
||||||
|
@ -60,7 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
{% if can_manage_payments %}
|
{% if is_broker %}
|
||||||
<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">
|
||||||
|
@ -104,7 +107,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-12 {% if can_manage_payments %}col-lg-7{% else %}col-lg-12{% endif %}">
|
<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 d-flex justify-content-between">
|
||||||
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
<h5 class="card-title mb-0">وضعیت پیشفاکتور</h5>
|
||||||
|
@ -161,7 +164,7 @@
|
||||||
{% for p in payments %}
|
{% for p in payments %}
|
||||||
<tr>
|
<tr>
|
||||||
<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.jpayment_date }}</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>
|
||||||
|
@ -171,7 +174,7 @@
|
||||||
<i class="bx bx-show"></i>
|
<i class="bx bx-show"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_manage_payments %}
|
{% if is_broker %}
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="openDeleteModal('{{ p.id }}')" title="حذف" aria-label="حذف">
|
<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>
|
||||||
|
@ -195,7 +198,7 @@
|
||||||
<h6 class="mb-0">وضعیت تاییدها</h6>
|
<h6 class="mb-0">وضعیت تاییدها</h6>
|
||||||
{% if can_approve_reject %}
|
{% if can_approve_reject %}
|
||||||
<div class="d-flex gap-2">
|
<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-success btn-sm" data-bs-toggle="modal" data-bs-target="#approvePaymentsModal2">تایید</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
|
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectPaymentsModal">رد</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -356,6 +359,13 @@
|
||||||
}
|
}
|
||||||
const form = document.getElementById('formAddPayment');
|
const form = document.getElementById('formAddPayment');
|
||||||
const fd = buildFormData(form);
|
const fd = buildFormData(form);
|
||||||
|
|
||||||
|
// تبدیل تاریخ شمسی به میلادی برای ارسال
|
||||||
|
const persianDateValue = $('#id_payment_date').val();
|
||||||
|
const gregorianDateValue = $('#id_payment_date').attr('data-gregorian');
|
||||||
|
if (persianDateValue && gregorianDateValue) {
|
||||||
|
fd.set('payment_date', gregorianDateValue);
|
||||||
|
}
|
||||||
fetch('{% url "invoices:add_quote_payment" instance.id step.id %}', {
|
fetch('{% url "invoices:add_quote_payment" instance.id step.id %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: fd
|
body: fd
|
||||||
|
@ -419,18 +429,24 @@
|
||||||
observer: true,
|
observer: true,
|
||||||
calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
|
calendar: { persian: { locale: 'fa', leapYearMode: 'astronomical' } },
|
||||||
onSelect: function(unix) {
|
onSelect: function(unix) {
|
||||||
|
// تبدیل تاریخ شمسی به میلادی برای ارسال به سرور
|
||||||
const gregorianDate = new Date(unix);
|
const gregorianDate = new Date(unix);
|
||||||
const year = gregorianDate.getFullYear();
|
const year = gregorianDate.getFullYear();
|
||||||
const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
|
const month = String(gregorianDate.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(gregorianDate.getDate()).padStart(2, '0');
|
const day = String(gregorianDate.getDate()).padStart(2, '0');
|
||||||
const gregorianDateString = `${year}-${month}-${day}`;
|
const gregorianDateString = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// نمایش تاریخ شمسی در فیلد
|
||||||
if (window.persianDate) {
|
if (window.persianDate) {
|
||||||
const persianDate = new window.persianDate(unix);
|
const persianDate = new window.persianDate(unix);
|
||||||
const persianDateString = persianDate.format('YYYY/MM/DD');
|
const persianDateString = persianDate.format('YYYY/MM/DD');
|
||||||
$('#id_payment_date').val(persianDateString);
|
$('#id_payment_date').val(persianDateString);
|
||||||
} else {
|
} else {
|
||||||
|
// اگر persianDate در دسترس نبود، تاریخ میلادی را نمایش بده
|
||||||
$('#id_payment_date').val(gregorianDateString);
|
$('#id_payment_date').val(gregorianDateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ذخیره تاریخ میلادی در فیلد مخفی برای ارسال به سرور
|
||||||
$('#id_payment_date').attr('data-gregorian', gregorianDateString);
|
$('#id_payment_date').attr('data-gregorian', gregorianDateString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include '_toasts.html' %}
|
{% include '_toasts.html' %}
|
||||||
|
|
||||||
|
<!-- Instance Info Modal -->
|
||||||
|
{% instance_info_modal instance %}
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -32,15 +36,17 @@
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
||||||
<small class="text-muted d-block">
|
<small class="text-muted d-block">
|
||||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
{% instance_info instance %}
|
||||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
|
<a href="{% url 'invoices:quote_print' instance.id %}" target="_blank" class="btn btn-outline-secondary">
|
||||||
<i class="bx bx-printer"></i> پرینت
|
<i class="bx bx-printer me-2"></i> پرینت
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
|
||||||
|
بازگشت
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -50,100 +56,116 @@
|
||||||
<!-- Invoice Preview Card -->
|
<!-- Invoice Preview Card -->
|
||||||
<div class="card invoice-preview-card mt-4 border">
|
<div class="card invoice-preview-card mt-4 border">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between flex-xl-row flex-md-column flex-sm-row flex-column p-sm-3 p-0">
|
<div class="d-flex justify-content-between flex-xl-row flex-md-column flex-sm-row flex-column p-sm-3 p-0 align-items-center">
|
||||||
<div class="mb-xl-0 mb-4">
|
<div class="mb-xl-0 mb-4">
|
||||||
<div class="d-flex svg-illustration mb-3 gap-2">
|
<!-- Company Logo & Info -->
|
||||||
<span class="app-brand-logo demo">
|
<div class="d-flex align-items-center">
|
||||||
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<div class="avatar avatar-lg me-3">
|
||||||
<defs>
|
<span class="avatar-initial rounded bg-label-primary">
|
||||||
<path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
|
{% if instance.broker.company %}
|
||||||
<path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
|
<img src="{{ instance.broker.company.logo.url }}" alt="لوگوی شرکت" style="max-height:80px;">
|
||||||
<path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
|
{% else %}
|
||||||
<path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
|
<i class="bx bx-buildings bx-md"></i>
|
||||||
</defs>
|
{% endif %}
|
||||||
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
|
|
||||||
<g id="Icon" transform="translate(27.000000, 15.000000)">
|
|
||||||
<g id="Mask" transform="translate(0.000000, 8.000000)">
|
|
||||||
<mask id="mask-2" fill="white">
|
|
||||||
<use xlink:href="#path-1"></use>
|
|
||||||
</mask>
|
|
||||||
<use fill="#696cff" xlink:href="#path-1"></use>
|
|
||||||
<g id="Path-3" mask="url(#mask-2)">
|
|
||||||
<use fill="#696cff" xlink:href="#path-3"></use>
|
|
||||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
|
|
||||||
</g>
|
|
||||||
<g id="Path-4" mask="url(#mask-2)">
|
|
||||||
<use fill="#696cff" xlink:href="#path-4"></use>
|
|
||||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
|
|
||||||
<use fill="#696cff" xlink:href="#path-5"></use>
|
|
||||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="app-brand-text demo text-body fw-bold">شرکت آب منطقهای</span>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">دفتر مرکزی، خیابان اصلی</p>
|
|
||||||
<p class="mb-1">تهران، ایران</p>
|
|
||||||
<p class="mb-0">۰۲۱-۱۲۳۴۵۶۷۸</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4>پیشفاکتور #{{ quote.name }}</h4>
|
<h5 class="mb-1">
|
||||||
<div class="mb-2">
|
{% if instance.broker.company %}
|
||||||
<span class="me-1">تاریخ صدور:</span>
|
{{ instance.broker.company.name }}
|
||||||
<span class="fw-medium">{{ quote.jcreated }}</span>
|
{% else %}
|
||||||
|
شرکت آب منطقهای
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
{% if instance.broker.company %}
|
||||||
|
<div class="text-muted small">
|
||||||
|
{% if instance.broker.company.address %}
|
||||||
|
<div><i class="bx bx-map me-1"></i>{{ instance.broker.company.address }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.affairs.county.city.name %}
|
||||||
|
<div><i class="bx bx-current-location me-1"></i>{{ instance.broker.affairs.county.city.name }}، ایران</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.phone %}
|
||||||
|
<div><i class="bx bx-phone me-1"></i>{{ instance.broker.company.phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Invoice Details -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-body">#{{ quote.name }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="invoice-details">
|
||||||
|
<div class="d-flex justify-content-end align-items-center mb-2">
|
||||||
|
<span class="text-muted me-2">تاریخ صدور:</span>
|
||||||
|
<span class="fw-medium text-body">{{ quote.jcreated_date }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span class="me-1">معتبر تا:</span>
|
|
||||||
<span class="fw-medium">{{ quote.valid_until|date:"Y/m/d" }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-0">
|
<hr class="my-0">
|
||||||
<div class="card-body">
|
<div class="card-body py-1">
|
||||||
<div class="row p-sm-3 p-0">
|
<div class="row">
|
||||||
<div class="col-xl-6 col-md-12 col-sm-5 col-12 mb-xl-0 mb-md-4 mb-sm-0 mb-4">
|
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
|
||||||
<h6 class="pb-2">صادر شده برای:</h6>
|
<div class="">
|
||||||
<p class="mb-1">{{ quote.customer.get_full_name }}</p>
|
<div class="card-body p-3">
|
||||||
{% if instance.representative.profile %}
|
<h6 class="card-title text-primary mb-2">
|
||||||
<p class="mb-1">کد ملی: {{ instance.representative.profile.national_code }}</p>
|
<i class="bx bx-user me-1"></i>اطلاعات مشترک
|
||||||
<p class="mb-1">{{ instance.representative.profile.address|default:"آدرس نامشخص" }}</p>
|
</h6>
|
||||||
<p class="mb-1">{{ instance.representative.profile.phone_number_1|default:"" }}</p>
|
<div class="d-flex gap-2 mb-1">
|
||||||
{% endif %}
|
<span class="text-muted small">نام:</span>
|
||||||
|
<span class="fw-medium small">{{ quote.customer.get_full_name }}</span>
|
||||||
|
</div>
|
||||||
|
{% if instance.representative.profile.national_code %}
|
||||||
|
<div class="d-flex gap-2 mb-1">
|
||||||
|
<span class="text-muted small">کد ملی:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.representative.profile.national_code }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.representative.profile.phone_number_1 %}
|
||||||
|
<div class="d-flex gap-2 mb-1">
|
||||||
|
<span class="text-muted small">تلفن:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.representative.profile.phone_number_1 }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.representative.profile.address %}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<span class="text-muted small">آدرس:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.representative.profile.address }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 col-md-12 col-sm-6 col-12 mb-3">
|
||||||
|
<div class="border-0 bg-light">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<h6 class="card-title text-primary mb-2">
|
||||||
|
<i class="bx bx-droplet me-1"></i>اطلاعات چاه
|
||||||
|
</h6>
|
||||||
|
<div class="d-flex gap-2 mb-1">
|
||||||
|
<span class="text-muted small">شماره اشتراک آب:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.well.water_subscription_number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 mb-1">
|
||||||
|
<span class="text-muted small">شماره اشتراک برق:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.well.electricity_subscription_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 mb-1">
|
||||||
|
<span class="text-muted small">سریال کنتور:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.well.water_meter_serial_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<span class="text-muted small">قدرت چاه:</span>
|
||||||
|
<span class="fw-medium small">{{ instance.well.well_power|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6 col-md-12 col-sm-7 col-12">
|
|
||||||
<h6 class="pb-2">اطلاعات چاه:</h6>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="pe-3">شماره اشتراک آب:</td>
|
|
||||||
<td>{{ instance.well.water_subscription_number }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="pe-3">شماره اشتراک برق:</td>
|
|
||||||
<td>{{ instance.well.electricity_subscription_number|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="pe-3">سریال کنتور:</td>
|
|
||||||
<td>{{ instance.well.water_meter_serial_number|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="pe-3">قدرت چاه:</td>
|
|
||||||
<td>{{ instance.well.well_power|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="pe-3">کد درخواست:</td>
|
|
||||||
<td>{{ instance.code }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,11 +192,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="align-top px-4 py-5">
|
<td colspan="3" class="align-top px-4 py-5">
|
||||||
<p class="mb-2">
|
|
||||||
<span class="me-1 fw-medium">صادر کننده:</span>
|
|
||||||
<span>{{ quote.created_by.get_full_name }}</span>
|
|
||||||
</p>
|
|
||||||
<span>با تشکر از انتخاب شما</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-4 py-5">
|
<td class="text-end px-4 py-5">
|
||||||
<p class="mb-2">جمع کل:</p>
|
<p class="mb-2">جمع کل:</p>
|
||||||
|
@ -193,6 +210,72 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<!-- Footer Information -->
|
||||||
|
<div class="card-body border-top">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h6 class="mb-3">شرایط و ضوابط:</h6>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bx bx-time-five text-muted me-2"></i>
|
||||||
|
اعتبار پیشفاکتور صادر شده ۴۸ ساعت پس از تاریخ صدور میباشد
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bx bx-money text-muted me-2"></i>
|
||||||
|
مبلغ فوق به صورت علیالحساب دریافت میگردد
|
||||||
|
</li>
|
||||||
|
<li class="mb-0">
|
||||||
|
<i class="bx bx-info-circle text-muted me-2"></i>
|
||||||
|
این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد
|
||||||
|
</li>
|
||||||
|
{% if instance.broker.company.signature %}
|
||||||
|
<li class="mb-0 text-start mt-4 ms-5">
|
||||||
|
<img src="{{ instance.broker.company.signature.url }}" alt="امضای شرکت" style="height: 200px;">
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if instance.broker.company %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6 class="mb-3">اطلاعات پرداخت:</h6>
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
{% if instance.broker.company.card_number %}
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">شماره کارت:</small>
|
||||||
|
<div class="fw-medium">{{ instance.broker.company.card_number }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.account_number %}
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">شماره حساب:</small>
|
||||||
|
<div class="fw-medium">{{ instance.broker.company.account_number }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.sheba_number %}
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">شماره شبا:</small>
|
||||||
|
<div class="fw-medium">{{ instance.broker.company.sheba_number }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.bank_name %}
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">بانک:</small>
|
||||||
|
<div class="fw-medium">{{ instance.broker.company.get_bank_name_display }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.branch_name %}
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">شعبه:</small>
|
||||||
|
<div class="fw-medium">{{ instance.broker.company.branch_name }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if quote.notes %}
|
{% if quote.notes %}
|
||||||
|
@ -240,7 +323,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{% 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 %}"
|
||||||
class="btn btn-label-primary">
|
class="btn btn-primary">
|
||||||
مرحله بعد
|
مرحله بعد
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -5,8 +5,24 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>پیشفاکتور {{ quote.name }} - {{ instance.code }}</title>
|
<title>پیشفاکتور {{ quote.name }} - {{ instance.code }}</title>
|
||||||
|
|
||||||
<!-- Bootstrap CSS -->
|
{% load static %}
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
{% load humanize %}
|
||||||
|
|
||||||
|
<!-- Fonts (match base) -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Icons (optional) -->
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/boxicons.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/fontawesome.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/fonts/flag-icons.css' %}">
|
||||||
|
|
||||||
|
<!-- Core CSS (same as preview) -->
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/core.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/vendor/css/rtl/theme-default.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/css/demo.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'assets/css/persian-fonts.css' %}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@page {
|
@page {
|
||||||
|
@ -14,11 +30,7 @@
|
||||||
margin: 1cm;
|
margin: 1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* Inherit project fonts and sizes from core.css/persian-fonts */
|
||||||
font-family: 'Vazirmatn', sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body { print-color-adjust: exact; }
|
body { print-color-adjust: exact; }
|
||||||
|
@ -27,7 +39,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-header {
|
.invoice-header {
|
||||||
border-bottom: 2px solid #696cff;
|
border-bottom: 1px solid #dee2e6;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
@ -89,195 +101,159 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Print Button (hidden in print) -->
|
<!-- Auto print: buttons removed -->
|
||||||
<div class="no-print mb-3">
|
|
||||||
<button onclick="window.print()" class="btn btn-primary">
|
|
||||||
<i class="bi bi-printer"></i> پرینت
|
|
||||||
</button>
|
|
||||||
<button onclick="window.close()" class="btn btn-secondary">
|
|
||||||
بستن
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Invoice Header -->
|
<!-- Invoice Header (compact, matches preview) -->
|
||||||
<div class="invoice-header">
|
<div class="invoice-header">
|
||||||
<div class="row">
|
<div class="row align-items-center">
|
||||||
<div class="col-6">
|
<div class="col-6 d-flex align-items-center">
|
||||||
<div class="company-logo mb-3">
|
<div class="me-3" style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;background:#eef2ff;border-radius:8px;">
|
||||||
شرکت آب منطقهای
|
{% if instance.broker.company and instance.broker.company.logo %}
|
||||||
|
<img src="{{ instance.broker.company.logo.url }}" alt="لوگو" style="max-height:58px;max-width:120px;">
|
||||||
|
{% else %}
|
||||||
|
<span class="company-logo">شرکت</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="company-info">
|
<div>
|
||||||
<p class="mb-1">دفتر مرکزی، خیابان اصلی</p>
|
{% if instance.broker.company %}
|
||||||
<p class="mb-1">تهران، ایران</p>
|
{{ instance.broker.company.name }}
|
||||||
<p class="mb-1">تلفن: ۰۲۱-۱۲۳۴۵۶۷۸</p>
|
{% endif %}
|
||||||
<p class="mb-0">ایمیل: info@watercompany.ir</p>
|
{% if instance.broker.company %}
|
||||||
|
<div class="text-muted small">
|
||||||
|
{% if instance.broker.company.address %}
|
||||||
|
<div>{{ instance.broker.company.address }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.affairs.county.city.name %}
|
||||||
|
<div>{{ instance.broker.affairs.county.city.name }}، ایران</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.phone %}
|
||||||
|
<div>تلفن: {{ instance.broker.company.phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 text-end">
|
<div class="col-6 text-end">
|
||||||
<div class="invoice-title">پیشفاکتور</div>
|
<div class="mt-2">
|
||||||
<div class="mt-3">
|
<div><strong>#{{ quote.name }}</strong></div>
|
||||||
<table class="info-table">
|
<div class="text-muted small">تاریخ صدور: {{ quote.jcreated_date }}</div>
|
||||||
<tr>
|
|
||||||
<td><strong>شماره پیشفاکتور:</strong></td>
|
|
||||||
<td>{{ quote.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>کد درخواست:</strong></td>
|
|
||||||
<td>{{ instance.code }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>تاریخ صدور:</strong></td>
|
|
||||||
<td>{{ quote.created|date:"Y/m/d" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>معتبر تا:</strong></td>
|
|
||||||
<td>{{ quote.valid_until|date:"Y/m/d" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Customer & Well Info -->
|
<!-- Customer & Well Info (compact to match preview) -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<h6 class="fw-bold mb-3">مشخصات مشترک:</h6>
|
<h6 class="fw-bold mb-2">اطلاعات مشترک</h6>
|
||||||
<table class="info-table">
|
<div class="small mb-1"><span class="text-muted">نام:</span> {{ quote.customer.get_full_name }}</div>
|
||||||
<tr>
|
{% if instance.representative.profile and instance.representative.profile.national_code %}
|
||||||
<td><strong>نام و نام خانوادگی:</strong></td>
|
<div class="small mb-1"><span class="text-muted">کد ملی:</span> {{ instance.representative.profile.national_code }}</div>
|
||||||
<td>{{ quote.customer.get_full_name }}</td>
|
{% endif %}
|
||||||
</tr>
|
{% if instance.representative.profile and instance.representative.profile.phone_number_1 %}
|
||||||
{% if instance.representative.profile %}
|
<div class="small mb-1"><span class="text-muted">تلفن:</span> {{ instance.representative.profile.phone_number_1 }}</div>
|
||||||
<tr>
|
{% endif %}
|
||||||
<td><strong>کد ملی:</strong></td>
|
{% if instance.representative.profile and instance.representative.profile.address %}
|
||||||
<td>{{ instance.representative.profile.national_code }}</td>
|
<div class="small"><span class="text-muted">آدرس:</span> {{ instance.representative.profile.address }}</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>تلفن:</strong></td>
|
|
||||||
<td>{{ instance.representative.profile.phone_number_1|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>آدرس:</strong></td>
|
|
||||||
<td>{{ instance.representative.profile.address|default:"آدرس نامشخص" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<h6 class="fw-bold mb-3">مشخصات چاه:</h6>
|
<h6 class="fw-bold mb-2">اطلاعات چاه</h6>
|
||||||
<table class="info-table">
|
<div class="small mb-1"><span class="text-muted">شماره اشتراک آب:</span> {{ instance.well.water_subscription_number }}</div>
|
||||||
<tr>
|
<div class="small mb-1"><span class="text-muted">شماره اشتراک برق:</span> {{ instance.well.electricity_subscription_number|default:"-" }}</div>
|
||||||
<td><strong>شماره اشتراک آب:</strong></td>
|
<div class="small mb-1"><span class="text-muted">سریال کنتور:</span> {{ instance.well.water_meter_serial_number|default:"-" }}</div>
|
||||||
<td>{{ instance.well.water_subscription_number }}</td>
|
<div class="small"><span class="text-muted">قدرت چاه:</span> {{ instance.well.well_power|default:"-" }}</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>شماره اشتراک برق:</strong></td>
|
|
||||||
<td>{{ instance.well.electricity_subscription_number|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>سریال کنتور:</strong></td>
|
|
||||||
<td>{{ instance.well.water_meter_serial_number|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>قدرت چاه:</strong></td>
|
|
||||||
<td>{{ instance.well.well_power|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Items Table -->
|
<!-- Items Table -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<table class="table items-table">
|
<table class="table border-top m-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 5%">ردیف</th>
|
<th style="width: 5%">ردیف</th>
|
||||||
<th style="width: 30%">شرح کالا/خدمات</th>
|
<th style="width: 30%">شرح کالا/خدمات</th>
|
||||||
<th style="width: 30%">توضیحات</th>
|
<th style="width: 30%">توضیحات</th>
|
||||||
<th style="width: 10%">تعداد</th>
|
<th style="width: 10%">تعداد</th>
|
||||||
<th style="width: 12.5%">قیمت واحد (تومان)</th>
|
<th style="width: 12.5%">قیمت واحد(تومان)</th>
|
||||||
<th style="width: 12.5%">قیمت کل (تومان)</th>
|
<th style="width: 12.5%">قیمت کل(تومان)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for quote_item in quote.items.all %}
|
{% for quote_item in quote.items.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ forloop.counter }}</td>
|
<td>{{ forloop.counter }}</td>
|
||||||
<td class="text-start">{{ quote_item.item.name }}</td>
|
<td class="text-nowrap">{{ quote_item.item.name }}</td>
|
||||||
<td class="text-start">{{ quote_item.item.description|default:"-" }}</td>
|
<td class="text-nowrap">{{ quote_item.item.description|default:"-" }}</td>
|
||||||
<td>{{ quote_item.quantity }}</td>
|
<td>{{ quote_item.quantity }}</td>
|
||||||
<td>{{ quote_item.unit_price|floatformat:0 }}</td>
|
<td>{{ quote_item.unit_price|floatformat:0|intcomma:False }}</td>
|
||||||
<td>{{ quote_item.total_price|floatformat:0 }}</td>
|
<td>{{ quote_item.total_price|floatformat:0|intcomma:False }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="total-section">
|
<tr class="total-section">
|
||||||
<td colspan="5" class="text-end"><strong>جمع کل:</strong></td>
|
<td colspan="5" class="text-end"><strong>جمع کل(تومان):</strong></td>
|
||||||
<td><strong>{{ quote.total_amount|floatformat:0 }} تومان</strong></td>
|
<td><strong>{{ quote.total_amount|floatformat:0|intcomma:False }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if quote.discount_amount > 0 %}
|
{% if quote.discount_amount > 0 %}
|
||||||
<tr class="total-section">
|
<tr class="total-section">
|
||||||
<td colspan="5" class="text-end"><strong>تخفیف:</strong></td>
|
<td colspan="5" class="text-end"><strong>تخفیف(تومان):</strong></td>
|
||||||
<td><strong>{{ quote.discount_amount|floatformat:0 }} تومان</strong></td>
|
<td><strong>{{ quote.discount_amount|floatformat:0|intcomma:False }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="total-section border-top border-2">
|
<tr class="total-section border-top border-2">
|
||||||
<td colspan="5" class="text-end"><strong>مبلغ نهایی:</strong></td>
|
<td colspan="5" class="text-end"><strong>مبلغ نهایی(تومان):</strong></td>
|
||||||
<td><strong>{{ quote.final_amount|floatformat:0 }} تومان</strong></td>
|
<td><strong>{{ quote.final_amount|floatformat:0|intcomma:False }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Conditions & Payment (matches preview) -->
|
||||||
{% if quote.notes %}
|
<div class="row">
|
||||||
<div class="mb-4">
|
<div class="col-8">
|
||||||
<h6 class="fw-bold">یادداشت:</h6>
|
<h6 class="fw-bold mb-2">شرایط و ضوابط</h6>
|
||||||
<p>{{ quote.notes }}</p>
|
<ul class="small mb-0">
|
||||||
|
<li class="mb-1">اعتبار پیشفاکتور صادر شده ۴۸ ساعت پس از تاریخ صدور میباشد</li>
|
||||||
|
<li class="mb-1">مبلغ فوق به صورت علیالحساب دریافت میگردد</li>
|
||||||
|
<li class="mb-1">این برگه صرفاً جهت اعلام قیمت بوده و ارزش قانونی دیگری ندارد</li>
|
||||||
|
{% if instance.broker.company and instance.broker.company.signature %}
|
||||||
|
<li class="mt-3" style="list-style:none;"><img src="{{ instance.broker.company.signature.url }}" alt="امضا" style="height: 200px;"></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if instance.broker.company %}
|
||||||
|
<div class="col-4">
|
||||||
|
<h6 class="fw-bold mb-2">اطلاعات پرداخت</h6>
|
||||||
|
{% if instance.broker.company.card_number %}
|
||||||
|
<div class="small mb-1"><span class="text-muted">شماره کارت:</span> {{ instance.broker.company.card_number }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.account_number %}
|
||||||
|
<div class="small mb-1"><span class="text-muted">شماره حساب:</span> {{ instance.broker.company.account_number }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.sheba_number %}
|
||||||
|
<div class="small mb-1"><span class="text-muted">شماره شبا:</span> {{ instance.broker.company.sheba_number }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.broker.company.bank_name %}
|
||||||
|
<div class="small"><span class="text-muted">بانک:</span> {{ instance.broker.company.get_bank_name_display }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Additional Info -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<p><strong>صادر کننده:</strong> {{ quote.created_by.get_full_name }}</p>
|
|
||||||
<p class="text-muted">این پیشفاکتور تا تاریخ {{ quote.valid_until|date:"Y/m/d" }} معتبر است.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Signature Section -->
|
<!-- Signature Section (optional, compact) -->
|
||||||
<div class="signature-section">
|
{% if quote.notes %}
|
||||||
<div class="row">
|
<div class="mt-3 small text-muted">یادداشت: {{ quote.notes }}</div>
|
||||||
<div class="col-6">
|
{% endif %}
|
||||||
<div class="text-center">
|
|
||||||
<p class="mb-2"><strong>امضای مشترک</strong></p>
|
|
||||||
<div class="signature-box">
|
|
||||||
امضا و مهر
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 small">تاریخ: ____/____/____</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="mb-2"><strong>امضای شرکت</strong></p>
|
|
||||||
<div class="signature-box">
|
|
||||||
امضا و مهر
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 small">تاریخ: ____/____/____</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="text-center mt-4 small text-muted">
|
|
||||||
این پیشفاکتور توسط سیستم مدیریت فرآیندها تولید شده است.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Auto print on load (optional)
|
window.onload = function() {
|
||||||
// window.onload = function() { window.print(); }
|
window.print();
|
||||||
|
setTimeout(function(){ window.close(); }, 200);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include '_toasts.html' %}
|
{% include '_toasts.html' %}
|
||||||
|
|
||||||
|
<!-- Instance Info Modal -->
|
||||||
|
{% instance_info_modal instance %}
|
||||||
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
|
@ -26,11 +30,13 @@
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
<h4 class="mb-1">{{ step.name }}: {{ instance.process.name }}</h4>
|
||||||
<small class="text-muted d-block">
|
<small class="text-muted d-block">
|
||||||
اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }}
|
{% instance_info instance %}
|
||||||
| نماینده: {{ instance.representative.profile.national_code|default:"-" }}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">بازگشت</a>
|
<a href="{% url 'processes:request_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bx bx-chevron-right bx-sm ms-sm-n2"></i>
|
||||||
|
بازگشت
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bs-stepper wizard-vertical vertical mt-2">
|
<div class="bs-stepper wizard-vertical vertical mt-2">
|
||||||
|
@ -50,7 +56,7 @@
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-3">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<h6>پیشفاکتور موجود</h6>
|
<h6>پیشفاکتور موجود</h6>
|
||||||
<span class="mb-1">نام: {{ existing_quote.name }} | </span>
|
<span class="mb-1">{{ existing_quote.name }} | </span>
|
||||||
<span class="mb-1">مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
|
<span class="mb-1">مبلغ کل: {{ existing_quote.final_amount|floatformat:0|intcomma:False }} تومان | </span>
|
||||||
<span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
|
<span class="mb-0">وضعیت: {{ existing_quote.get_status_display_with_color|safe }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,7 +149,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if next_step %}
|
{% if next_step %}
|
||||||
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-label-primary">
|
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">
|
||||||
مرحله بعد
|
مرحله بعد
|
||||||
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
<i class="bx bx-chevron-left bx-sm me-sm-n2"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -212,4 +218,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,9 +15,7 @@ urlpatterns = [
|
||||||
# Quote payments step (step 3)
|
# Quote payments step (step 3)
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/payments/', views.quote_payment_step, name='quote_payment_step'),
|
path('instance/<int:instance_id>/step/<int:step_id>/payments/', views.quote_payment_step, name='quote_payment_step'),
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/payments/add/', views.add_quote_payment, name='add_quote_payment'),
|
path('instance/<int:instance_id>/step/<int:step_id>/payments/add/', views.add_quote_payment, name='add_quote_payment'),
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/payments/<int:payment_id>/update/', views.update_quote_payment, name='update_quote_payment'),
|
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/payments/<int:payment_id>/delete/', views.delete_quote_payment, name='delete_quote_payment'),
|
path('instance/<int:instance_id>/step/<int:step_id>/payments/<int:payment_id>/delete/', views.delete_quote_payment, name='delete_quote_payment'),
|
||||||
path('instance/<int:instance_id>/step/<int:step_id>/payments/approve/', views.approve_payments, name='approve_payments'),
|
|
||||||
|
|
||||||
# Quote print
|
# Quote print
|
||||||
path('instance/<int:instance_id>/quote/print/', views.quote_print, name='quote_print'),
|
path('instance/<int:instance_id>/quote/print/', views.quote_print, name='quote_print'),
|
||||||
|
|
|
@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def quote_step(request, instance_id, step_id):
|
def quote_step(request, instance_id, step_id):
|
||||||
"""مرحله انتخاب اقلام و ساخت پیشفاکتور"""
|
"""مرحله انتخاب اقلام و ساخت پیشفاکتور"""
|
||||||
|
@ -62,6 +63,7 @@ def quote_step(request, instance_id, step_id):
|
||||||
'is_broker': is_broker,
|
'is_broker': is_broker,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def create_quote(request, instance_id, step_id):
|
def create_quote(request, instance_id, step_id):
|
||||||
|
@ -90,7 +92,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, is_deleted=False, is_special=False).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:
|
||||||
|
@ -105,7 +107,7 @@ def create_quote(request, instance_id, step_id):
|
||||||
return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
|
return JsonResponse({'success': False, 'message': 'هیچ آیتمی انتخاب نشده است'})
|
||||||
|
|
||||||
# Create or reuse quote
|
# Create or reuse quote
|
||||||
quote, _ = Quote.objects.get_or_create(
|
quote, created_q = Quote.objects.get_or_create(
|
||||||
process_instance=instance,
|
process_instance=instance,
|
||||||
defaults={
|
defaults={
|
||||||
'name': f"پیشفاکتور {instance.code}",
|
'name': f"پیشفاکتور {instance.code}",
|
||||||
|
@ -115,6 +117,15 @@ def create_quote(request, instance_id, step_id):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Track whether this step was already completed before this edit
|
||||||
|
step_instance_existing = instance.step_instances.filter(step=step).first()
|
||||||
|
was_already_completed = bool(step_instance_existing and step_instance_existing.status == 'completed')
|
||||||
|
|
||||||
|
# Snapshot previous items before overwrite for change detection
|
||||||
|
previous_items_map = {}
|
||||||
|
if not created_q:
|
||||||
|
previous_items_map = {qi.item_id: int(qi.quantity) for qi in quote.items.filter(is_deleted=False).all()}
|
||||||
|
|
||||||
# Replace quote items with submitted ones
|
# Replace quote items with submitted ones
|
||||||
quote.items.all().delete()
|
quote.items.all().delete()
|
||||||
for entry in items_payload:
|
for entry in items_payload:
|
||||||
|
@ -138,31 +149,78 @@ def create_quote(request, instance_id, step_id):
|
||||||
|
|
||||||
quote.calculate_totals()
|
quote.calculate_totals()
|
||||||
|
|
||||||
|
# Detect changes versus previous state and mark audit fields if editing after completion
|
||||||
|
try:
|
||||||
|
new_items_map = {int(entry.get('id')): int(entry.get('qty') or 1) for entry in items_payload}
|
||||||
|
except Exception:
|
||||||
|
new_items_map = {}
|
||||||
|
|
||||||
|
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
||||||
|
|
||||||
|
if was_already_completed and new_items_map != previous_items_map:
|
||||||
|
# StepInstance-level generic audit (for reuse across steps)
|
||||||
|
if step_instance_existing:
|
||||||
|
step_instance_existing.edited_after_completion = True
|
||||||
|
step_instance_existing.last_edited_at = timezone.now()
|
||||||
|
step_instance_existing.last_edited_by = request.user
|
||||||
|
step_instance_existing.edit_count = (step_instance_existing.edit_count or 0) + 1
|
||||||
|
step_instance_existing.completed_at = timezone.now()
|
||||||
|
step_instance_existing.save(update_fields=['edited_after_completion', 'last_edited_at', 'last_edited_by', 'edit_count', 'completed_at'])
|
||||||
|
|
||||||
|
|
||||||
|
if quote.status != 'draft':
|
||||||
|
quote.status = 'draft'
|
||||||
|
quote.save(update_fields=['status'])
|
||||||
|
|
||||||
|
# Reset ALL subsequent completed steps to in_progress
|
||||||
|
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
|
||||||
|
for subsequent_step in subsequent_steps:
|
||||||
|
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
|
||||||
|
if subsequent_step_instance and subsequent_step_instance.status == 'completed':
|
||||||
|
# Bypass validation by using update() instead of save()
|
||||||
|
instance.step_instances.filter(step=subsequent_step).update(
|
||||||
|
status='in_progress',
|
||||||
|
completed_at=None
|
||||||
|
)
|
||||||
|
# Clear previous approvals if the step requires re-approval
|
||||||
|
try:
|
||||||
|
subsequent_step_instance.approvals.all().delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Set current step to the next step
|
||||||
|
if next_step:
|
||||||
|
instance.current_step = next_step
|
||||||
|
instance.save(update_fields=['current_step'])
|
||||||
|
|
||||||
# تکمیل مرحله
|
# تکمیل مرحله
|
||||||
step_instance, created = StepInstance.objects.get_or_create(
|
step_instance, created = StepInstance.objects.get_or_create(
|
||||||
process_instance=instance,
|
process_instance=instance,
|
||||||
step=step
|
step=step
|
||||||
)
|
)
|
||||||
|
if not was_already_completed:
|
||||||
step_instance.status = 'completed'
|
step_instance.status = 'completed'
|
||||||
step_instance.completed_at = timezone.now()
|
step_instance.completed_at = timezone.now()
|
||||||
step_instance.save()
|
step_instance.save(update_fields=['status', 'completed_at'])
|
||||||
|
|
||||||
# انتقال به مرحله بعدی
|
# انتقال به مرحله بعدی
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
|
||||||
redirect_url = None
|
redirect_url = None
|
||||||
if next_step:
|
if next_step:
|
||||||
|
# Only advance current step if we are currently on this step to avoid regressions
|
||||||
|
if instance.current_step_id == step.id:
|
||||||
instance.current_step = next_step
|
instance.current_step = next_step
|
||||||
instance.save()
|
instance.save(update_fields=['current_step'])
|
||||||
# هدایت مستقیم به مرحله پیشنمایش پیشفاکتور
|
# هدایت مستقیم به مرحله پیشنمایش پیشفاکتور
|
||||||
redirect_url = reverse('invoices:quote_preview_step', args=[instance.id, next_step.id])
|
redirect_url = reverse('invoices:quote_preview_step', args=[instance.id, next_step.id])
|
||||||
|
|
||||||
return JsonResponse({'success': True, 'quote_id': quote.id, 'redirect': redirect_url})
|
return JsonResponse({'success': True, 'quote_id': quote.id, 'redirect': redirect_url})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def quote_preview_step(request, instance_id, step_id):
|
def quote_preview_step(request, instance_id, step_id):
|
||||||
"""مرحله صدور پیشفاکتور - نمایش و تایید فاکتور"""
|
"""مرحله صدور پیشفاکتور - نمایش و تایید فاکتور"""
|
||||||
instance = get_object_or_404(
|
instance = get_object_or_404(
|
||||||
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
|
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile', 'broker', 'broker__company', 'broker__affairs', 'broker__affairs__county', 'broker__affairs__county__city'),
|
||||||
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)
|
||||||
|
@ -199,6 +257,7 @@ def quote_preview_step(request, instance_id, step_id):
|
||||||
'is_broker': is_broker,
|
'is_broker': is_broker,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def quote_print(request, instance_id):
|
def quote_print(request, instance_id):
|
||||||
"""صفحه پرینت پیشفاکتور"""
|
"""صفحه پرینت پیشفاکتور"""
|
||||||
|
@ -210,6 +269,7 @@ def quote_print(request, instance_id):
|
||||||
'quote': quote,
|
'quote': quote,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def approve_quote(request, instance_id, step_id):
|
def approve_quote(request, instance_id, step_id):
|
||||||
|
@ -282,6 +342,7 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
}
|
}
|
||||||
|
|
||||||
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
|
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step, defaults={'status': 'in_progress'})
|
||||||
|
|
||||||
reqs = list(step.approver_requirements.select_related('role').all())
|
reqs = list(step.approver_requirements.select_related('role').all())
|
||||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
|
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_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
|
||||||
|
@ -295,6 +356,7 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
}
|
}
|
||||||
for r in reqs
|
for r in reqs
|
||||||
]
|
]
|
||||||
|
|
||||||
# dynamic permission: who can approve/reject this step (based on requirements)
|
# dynamic permission: who can approve/reject this step (based on requirements)
|
||||||
try:
|
try:
|
||||||
req_role_ids = {r.role_id for r in reqs}
|
req_role_ids = {r.role_id for r in reqs}
|
||||||
|
@ -302,20 +364,7 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
|
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
|
||||||
except Exception:
|
except Exception:
|
||||||
can_approve_reject = False
|
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)
|
# Accountant/Admin approval and rejection via POST (multi-role)
|
||||||
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
|
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
|
||||||
|
@ -359,6 +408,13 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
|
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
|
||||||
)
|
)
|
||||||
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
|
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
|
||||||
|
# If current step is ahead of this step, reset it back to this step
|
||||||
|
try:
|
||||||
|
if instance.current_step and instance.current_step.order > step.order:
|
||||||
|
instance.current_step = step
|
||||||
|
instance.save(update_fields=['current_step'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
|
messages.success(request, 'مرحله پرداختها رد شد و برای اصلاح بازگشت.')
|
||||||
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
return redirect('invoices:quote_payment_step', instance_id=instance.id, step_id=step.id)
|
||||||
|
|
||||||
|
@ -385,8 +441,6 @@ def quote_payment_step(request, instance_id, step_id):
|
||||||
'approver_statuses': approver_statuses,
|
'approver_statuses': approver_statuses,
|
||||||
'is_broker': is_broker,
|
'is_broker': is_broker,
|
||||||
'is_accountant': is_accountant,
|
'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,
|
'can_approve_reject': can_approve_reject,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -409,14 +463,16 @@ def add_quote_payment(request, instance_id, step_id):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# dynamic permission: users whose roles are among required approvers can add payments
|
# who can add payments
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
is_broker = False
|
||||||
|
is_accountant = False
|
||||||
try:
|
try:
|
||||||
req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
|
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
|
is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
|
||||||
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:
|
except Exception:
|
||||||
|
is_broker = False
|
||||||
|
is_accountant = False
|
||||||
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -474,48 +530,31 @@ def add_quote_payment(request, instance_id, step_id):
|
||||||
si.approvals.all().delete()
|
si.approvals.all().delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
redirect_url = reverse('invoices:quote_payment_step', args=[instance.id, step.id])
|
|
||||||
return JsonResponse({'success': True, 'redirect': redirect_url})
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
@login_required
|
|
||||||
def update_quote_payment(request, instance_id, step_id, payment_id):
|
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
|
||||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
||||||
quote = get_object_or_404(Quote, process_instance=instance)
|
|
||||||
invoice = Invoice.objects.filter(quote=quote).first()
|
|
||||||
if not invoice:
|
|
||||||
return JsonResponse({'success': False, 'message': 'فاکتور یافت نشد'})
|
|
||||||
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)
|
|
||||||
|
|
||||||
|
# Reset ALL subsequent completed steps to in_progress
|
||||||
try:
|
try:
|
||||||
amount = request.POST.get('amount')
|
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
|
||||||
payment_date = request.POST.get('payment_date') or payment.payment_date
|
for subsequent_step in subsequent_steps:
|
||||||
payment_method = request.POST.get('payment_method') or payment.payment_method
|
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
|
||||||
reference_number = request.POST.get('reference_number') or ''
|
if subsequent_step_instance and subsequent_step_instance.status == 'completed':
|
||||||
notes = request.POST.get('notes') or ''
|
# Bypass validation by using update() instead of save()
|
||||||
receipt_image = request.FILES.get('receipt_image')
|
instance.step_instances.filter(step=subsequent_step).update(
|
||||||
if amount:
|
status='in_progress',
|
||||||
payment.amount = amount
|
completed_at=None
|
||||||
payment.payment_date = payment_date
|
)
|
||||||
payment.payment_method = payment_method
|
# Clear previous approvals if the step requires re-approval
|
||||||
payment.reference_number = reference_number
|
try:
|
||||||
payment.notes = notes
|
subsequent_step_instance.approvals.all().delete()
|
||||||
# اگر نیاز به ذخیره عکس در Payment دارید، فیلد آن اضافه شده است
|
|
||||||
if receipt_image:
|
|
||||||
payment.receipt_image = receipt_image
|
|
||||||
payment.save()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return JsonResponse({'success': False, 'message': 'خطا در ویرایش فیش'})
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# On update, return to awaiting approval
|
# If current step is ahead of this step, reset it back to this step
|
||||||
try:
|
try:
|
||||||
si, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
if instance.current_step and instance.current_step.order > step.order:
|
||||||
si.status = 'in_progress'
|
instance.current_step = step
|
||||||
si.completed_at = None
|
instance.save(update_fields=['current_step'])
|
||||||
si.save()
|
|
||||||
si.approvals.all().delete()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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])
|
||||||
|
@ -532,15 +571,18 @@ 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
|
|
||||||
|
# who can delete payments
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
is_broker = False
|
||||||
|
is_accountant = False
|
||||||
try:
|
try:
|
||||||
req_role_ids = set(step.approver_requirements.values_list('role_id', flat=True))
|
is_broker = bool(profile and profile.has_role(UserRoles.BROKER))
|
||||||
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none())
|
is_accountant = bool(profile and profile.has_role(UserRoles.ACCOUNTANT))
|
||||||
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:
|
except Exception:
|
||||||
return JsonResponse({'success': False, 'message': 'شما مجوز حذف فیش را ندارید'})
|
is_broker = False
|
||||||
|
is_accountant = False
|
||||||
|
return JsonResponse({'success': False, 'message': 'شما مجوز افزودن فیش را ندارید'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# soft delete using project's BaseModel delete override
|
# soft delete using project's BaseModel delete override
|
||||||
|
@ -556,43 +598,37 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
|
||||||
si.approvals.all().delete()
|
si.approvals.all().delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Reset ALL subsequent completed steps to in_progress
|
||||||
|
try:
|
||||||
|
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
|
||||||
|
for subsequent_step in subsequent_steps:
|
||||||
|
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
|
||||||
|
if subsequent_step_instance and subsequent_step_instance.status == 'completed':
|
||||||
|
# Bypass validation by using update() instead of save()
|
||||||
|
instance.step_instances.filter(step=subsequent_step).update(
|
||||||
|
status='in_progress',
|
||||||
|
completed_at=None
|
||||||
|
)
|
||||||
|
# Clear previous approvals if the step requires re-approval
|
||||||
|
try:
|
||||||
|
subsequent_step_instance.approvals.all().delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If current step is ahead of this step, reset it back to this step
|
||||||
|
try:
|
||||||
|
if instance.current_step and instance.current_step.order > step.order:
|
||||||
|
instance.current_step = step
|
||||||
|
instance.save(update_fields=['current_step'])
|
||||||
|
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})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
@login_required
|
|
||||||
def approve_payments(request, instance_id, step_id):
|
|
||||||
"""تایید مرحله پرداخت فیشها و انتقال به مرحله بعد"""
|
|
||||||
instance = get_object_or_404(ProcessInstance, id=instance_id)
|
|
||||||
step = get_object_or_404(instance.process.steps, id=step_id)
|
|
||||||
quote = get_object_or_404(Quote, process_instance=instance)
|
|
||||||
|
|
||||||
is_fully_paid = quote.get_remaining_amount() <= 0
|
|
||||||
|
|
||||||
# تکمیل مرحله
|
|
||||||
step_instance, _ = StepInstance.objects.get_or_create(process_instance=instance, step=step)
|
|
||||||
step_instance.status = 'completed'
|
|
||||||
step_instance.completed_at = timezone.now()
|
|
||||||
step_instance.save()
|
|
||||||
|
|
||||||
# حرکت به مرحله بعد
|
|
||||||
next_step = instance.process.steps.filter(order__gt=step.order).first()
|
|
||||||
redirect_url = reverse('processes:request_list')
|
|
||||||
if next_step:
|
|
||||||
instance.current_step = next_step
|
|
||||||
instance.save()
|
|
||||||
redirect_url = reverse('processes:step_detail', args=[instance.id, next_step.id])
|
|
||||||
|
|
||||||
msg = 'پرداختها تایید شد'
|
|
||||||
if is_fully_paid:
|
|
||||||
msg += ' - مبلغ پیشفاکتور به طور کامل پرداخت شده است.'
|
|
||||||
else:
|
|
||||||
msg += ' - توجه: مبلغ پیشفاکتور به طور کامل پرداخت نشده است.'
|
|
||||||
|
|
||||||
return JsonResponse({'success': True, 'message': msg, 'redirect': redirect_url, 'is_fully_paid': is_fully_paid})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def final_invoice_step(request, instance_id, step_id):
|
def final_invoice_step(request, instance_id, step_id):
|
||||||
"""تجمیع اقلام پیشفاکتور با تغییرات نصب و صدور فاکتور نهایی"""
|
"""تجمیع اقلام پیشفاکتور با تغییرات نصب و صدور فاکتور نهایی"""
|
||||||
|
@ -1010,6 +1046,25 @@ def add_final_payment(request, instance_id, step_id):
|
||||||
si.save()
|
si.save()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Reset ALL subsequent completed steps to in_progress
|
||||||
|
try:
|
||||||
|
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
|
||||||
|
for subsequent_step in subsequent_steps:
|
||||||
|
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
|
||||||
|
if subsequent_step_instance and subsequent_step_instance.status == 'completed':
|
||||||
|
# Bypass validation by using update() instead of save()
|
||||||
|
instance.step_instances.filter(step=subsequent_step).update(
|
||||||
|
status='in_progress',
|
||||||
|
completed_at=None
|
||||||
|
)
|
||||||
|
# Clear previous approvals if the step requires re-approval
|
||||||
|
try:
|
||||||
|
subsequent_step_instance.approvals.all().delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
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]),
|
||||||
|
|
|
@ -29,7 +29,7 @@ class AffairsAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Broker)
|
@admin.register(Broker)
|
||||||
class BrokerAdmin(admin.ModelAdmin):
|
class BrokerAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'affairs', 'slug']
|
list_display = ['name', 'affairs', 'slug']
|
||||||
list_filter = ['affairs__county__city', 'affairs__county', 'affairs']
|
list_filter = ['affairs__county__city', 'affairs__county', 'affairs' ]
|
||||||
search_fields = ['name', 'affairs__name', 'affairs__county__name']
|
search_fields = ['name', 'affairs__name', 'affairs__county__name']
|
||||||
readonly_fields = ['deleted_at']
|
readonly_fields = ['deleted_at']
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
|
20
locations/migrations/0002_broker_company.py
Normal file
20
locations/migrations/0002_broker_company.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 10:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
('locations', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='broker',
|
||||||
|
name='company',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.company', verbose_name='شرکت'),
|
||||||
|
),
|
||||||
|
]
|
17
locations/migrations/0003_remove_broker_company.py
Normal file
17
locations/migrations/0003_remove_broker_company.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 13:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('locations', '0002_broker_company'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='broker',
|
||||||
|
name='company',
|
||||||
|
),
|
||||||
|
]
|
|
@ -50,10 +50,12 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||||
verbose_name_plural = "درخواستها"
|
verbose_name_plural = "درخواستها"
|
||||||
list_display = [
|
list_display = [
|
||||||
'code',
|
'code',
|
||||||
|
'current_step',
|
||||||
'slug',
|
'slug',
|
||||||
'well_display',
|
'well_display',
|
||||||
'representative',
|
'representative',
|
||||||
'requester',
|
'requester',
|
||||||
|
'broker',
|
||||||
'process',
|
'process',
|
||||||
'status_display',
|
'status_display',
|
||||||
'priority_display',
|
'priority_display',
|
||||||
|
@ -65,7 +67,8 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||||
'status',
|
'status',
|
||||||
'priority',
|
'priority',
|
||||||
'created',
|
'created',
|
||||||
'well__representative'
|
'well__representative',
|
||||||
|
'broker'
|
||||||
]
|
]
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'code',
|
'code',
|
||||||
|
@ -86,6 +89,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||||
'well',
|
'well',
|
||||||
'representative',
|
'representative',
|
||||||
'requester',
|
'requester',
|
||||||
|
'broker',
|
||||||
'process',
|
'process',
|
||||||
'current_step'
|
'current_step'
|
||||||
]
|
]
|
||||||
|
@ -99,7 +103,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||||
'fields': ('well', 'representative')
|
'fields': ('well', 'representative')
|
||||||
}),
|
}),
|
||||||
('اطلاعات درخواست', {
|
('اطلاعات درخواست', {
|
||||||
'fields': ('requester', 'priority')
|
'fields': ('requester', 'broker', 'priority')
|
||||||
}),
|
}),
|
||||||
('وضعیت و پیشرفت', {
|
('وضعیت و پیشرفت', {
|
||||||
'fields': ('status', 'current_step')
|
'fields': ('status', 'current_step')
|
||||||
|
@ -139,7 +143,7 @@ class ProcessInstanceAdmin(SimpleHistoryAdmin):
|
||||||
|
|
||||||
@admin.register(StepInstance)
|
@admin.register(StepInstance)
|
||||||
class StepInstanceAdmin(SimpleHistoryAdmin):
|
class StepInstanceAdmin(SimpleHistoryAdmin):
|
||||||
list_display = ['process_instance', 'step', 'assigned_to', 'status_display', 'rejection_count', 'started_at', 'completed_at']
|
list_display = ['process_instance', 'step', 'assigned_to', 'status_display', 'rejection_count', 'edit_count', 'started_at', 'completed_at']
|
||||||
list_filter = ['status', 'step__process', 'started_at']
|
list_filter = ['status', 'step__process', 'started_at']
|
||||||
search_fields = ['process_instance__name', 'step__name', 'assigned_to__username']
|
search_fields = ['process_instance__name', 'step__name', 'assigned_to__username']
|
||||||
readonly_fields = ['started_at', 'completed_at']
|
readonly_fields = ['started_at', 'completed_at']
|
||||||
|
|
20
processes/migrations/0002_processinstance_broker.py
Normal file
20
processes/migrations/0002_processinstance_broker.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-07 13:43
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('locations', '0003_remove_broker_company'),
|
||||||
|
('processes', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='processinstance',
|
||||||
|
name='broker',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='process_instances', to='locations.broker', verbose_name='کارگزار'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-09-08 08:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('processes', '0002_processinstance_broker'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalstepinstance',
|
||||||
|
name='edit_count',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='تعداد ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalstepinstance',
|
||||||
|
name='edited_after_completion',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalstepinstance',
|
||||||
|
name='last_edited_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='آخرین زمان ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalstepinstance',
|
||||||
|
name='last_edited_by',
|
||||||
|
field=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='ویرایش توسط'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stepinstance',
|
||||||
|
name='edit_count',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='تعداد ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stepinstance',
|
||||||
|
name='edited_after_completion',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stepinstance',
|
||||||
|
name='last_edited_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='آخرین زمان ویرایش پس از تکمیل'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stepinstance',
|
||||||
|
name='last_edited_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='step_instances_edited', to=settings.AUTH_USER_MODEL, verbose_name='ویرایش توسط'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,7 +5,7 @@ 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 django.conf import settings
|
||||||
from accounts.models import Role
|
from accounts.models import Role, Broker
|
||||||
from _helpers.utils import generate_unique_slug
|
from _helpers.utils import generate_unique_slug
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
@ -189,6 +189,15 @@ class ProcessInstance(SluggedModel):
|
||||||
verbose_name="اولویت"
|
verbose_name="اولویت"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
broker = models.ForeignKey(
|
||||||
|
Broker,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name="کارگزار",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name='process_instances'
|
||||||
|
)
|
||||||
|
|
||||||
completed_at = models.DateTimeField(
|
completed_at = models.DateTimeField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -205,13 +214,6 @@ class ProcessInstance(SluggedModel):
|
||||||
return f"{self.process.name} - {self.well.water_subscription_number}"
|
return f"{self.process.name} - {self.well.water_subscription_number}"
|
||||||
return f"{self.process.name} - {self.requester.get_full_name()}"
|
return f"{self.process.name} - {self.requester.get_full_name()}"
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""اعتبارسنجی مدل"""
|
|
||||||
if self.well and self.representative and self.well.representative != self.representative:
|
|
||||||
raise ValidationError("نماینده درخواست باید همان نماینده ثبت شده در چاه باشد")
|
|
||||||
|
|
||||||
if self.well and self.representative and self.requester == self.representative:
|
|
||||||
raise ValidationError("درخواست کننده نمیتواند نماینده چاه باشد")
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Generate unique 5-digit numeric code if missing
|
# Generate unique 5-digit numeric code if missing
|
||||||
|
@ -233,6 +235,13 @@ class ProcessInstance(SluggedModel):
|
||||||
if self.status == 'completed' and not self.completed_at:
|
if self.status == 'completed' and not self.completed_at:
|
||||||
self.completed_at = timezone.now()
|
self.completed_at = timezone.now()
|
||||||
|
|
||||||
|
# Auto-set broker if not already set
|
||||||
|
if not self.broker:
|
||||||
|
if self.well and hasattr(self.well, 'broker') and self.well.broker:
|
||||||
|
self.broker = self.well.broker
|
||||||
|
elif self.requester and hasattr(self.requester, 'profile') and self.requester.profile and hasattr(self.requester.profile, 'broker') and self.requester.profile.broker:
|
||||||
|
self.broker = self.requester.profile.broker
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_status_display_with_color(self):
|
def get_status_display_with_color(self):
|
||||||
|
@ -318,6 +327,12 @@ class StepInstance(models.Model):
|
||||||
notes = models.TextField(verbose_name="یادداشتها", blank=True)
|
notes = models.TextField(verbose_name="یادداشتها", blank=True)
|
||||||
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
|
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
|
||||||
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
|
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
|
||||||
|
# Generic edit-tracking for post-completion modifications
|
||||||
|
edited_after_completion = models.BooleanField(default=False, verbose_name="ویرایش پس از تکمیل")
|
||||||
|
last_edited_at = models.DateTimeField(null=True, blank=True, verbose_name="آخرین زمان ویرایش پس از تکمیل")
|
||||||
|
last_edited_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='step_instances_edited', verbose_name="ویرایش توسط")
|
||||||
|
edit_count = models.PositiveIntegerField(default=0, verbose_name="تعداد ویرایش پس از تکمیل")
|
||||||
|
|
||||||
history = HistoricalRecords()
|
history = HistoricalRecords()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
275
processes/templates/processes/includes/instance_info_modal.html
Normal file
275
processes/templates/processes/includes/instance_info_modal.html
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
{% load common_tags %}
|
||||||
|
|
||||||
|
<!-- Modal for Instance Info -->
|
||||||
|
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">اطلاعات درخواست {{ instance.code }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Well Information -->
|
||||||
|
{% if well %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-header bg-label-primary text-white py-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bx bx-water me-2"></i>اطلاعات چاه
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-droplet text-primary me-2"></i>
|
||||||
|
<strong>شماره اشتراک آب:</strong>
|
||||||
|
<span class="ms-2">{{ well.water_subscription_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-bolt text-warning me-2"></i>
|
||||||
|
<strong>شماره اشتراک برق:</strong>
|
||||||
|
<span class="ms-2">{{ well.electricity_subscription_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-barcode text-info me-2"></i>
|
||||||
|
<strong>سریال کنتور:</strong>
|
||||||
|
<span class="ms-2">{{ well.water_meter_serial_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-barcode-reader text-secondary me-2"></i>
|
||||||
|
<strong>سریال قدیمی:</strong>
|
||||||
|
<span class="ms-2">{{ well.water_meter_old_serial_number|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if well.water_meter_manufacturer %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-factory text-success me-2"></i>
|
||||||
|
<strong>سازنده کنتور:</strong>
|
||||||
|
<span class="ms-2">{{ well.water_meter_manufacturer.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-tachometer text-danger me-2"></i>
|
||||||
|
<strong>قدرت چاه:</strong>
|
||||||
|
<span class="ms-2">{{ well.well_power|default:"-" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if well.utm_x and well.utm_y %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-map text-info me-2"></i>
|
||||||
|
<strong>مختصات:</strong>
|
||||||
|
<span class="ms-2">X: {{ well.utm_x }}, Y: {{ well.utm_y }}</span>
|
||||||
|
{% if well.utm_zone %}<span class="text-muted ms-2">(Zone: {{ well.utm_zone }})</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if well.county %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-map-pin text-warning me-2"></i>
|
||||||
|
<strong>شهرستان:</strong>
|
||||||
|
<span class="ms-2">{{ well.county }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if well.affairs %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-building text-primary me-2"></i>
|
||||||
|
<strong>امور:</strong>
|
||||||
|
<span class="ms-2">{{ well.affairs }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if well.reference_letter_number %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-file text-secondary me-2"></i>
|
||||||
|
<strong>شماره معرفی نامه:</strong>
|
||||||
|
<span class="ms-2">{{ well.reference_letter_number }}</span>
|
||||||
|
{% if well.reference_letter_date %}
|
||||||
|
<span class="text-muted ms-2">({{ well.reference_letter_date|to_jalali }})</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if well.representative_letter_file %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-file text-secondary me-2"></i>
|
||||||
|
<strong>فایل نامه نمایندگی:</strong>
|
||||||
|
<a href="{{ well.representative_letter_file.url }}" class="ms-2">دانلود</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Representative Information -->
|
||||||
|
{% if representative %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-header bg-label-success text-white py-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bx bx-user me-2"></i>اطلاعات نماینده
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-user-circle text-primary me-2"></i>
|
||||||
|
<strong>نام و نام خانوادگی:</strong>
|
||||||
|
<span class="ms-2">{{ representative.get_full_name|default:representative.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if representative.profile.national_code %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-id-card text-info me-2"></i>
|
||||||
|
<strong>کد ملی:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.national_code }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.phone_number_1 %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-phone text-success me-2"></i>
|
||||||
|
<strong>تلفن اول:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.phone_number_1 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.phone_number_2 %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-phone text-success me-2"></i>
|
||||||
|
<strong>تلفن دوم:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.phone_number_2 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.bank_name %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-credit-card text-warning me-2"></i>
|
||||||
|
<strong>بانک:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.get_bank_name_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.card_number %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-credit-card-alt text-secondary me-2"></i>
|
||||||
|
<strong>شماره کارت:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.card_number }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.account_number %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-wallet text-info me-2"></i>
|
||||||
|
<strong>شماره حساب:</strong>
|
||||||
|
<span class="ms-2">{{ representative.profile.account_number }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if representative.profile.address %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-start mb-2">
|
||||||
|
<i class="bx bx-map text-danger me-2 mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<strong>آدرس:</strong>
|
||||||
|
<p class="mb-0 ms-2 text-wrap">{{ representative.profile.address }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Process Information -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-header bg-label-info text-white py-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bx bx-cog me-2"></i>اطلاعات فرآیند
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-list-ul text-primary me-2"></i>
|
||||||
|
<strong>نوع فرآیند:</strong>
|
||||||
|
<span class="ms-2">{{ instance.process.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-calendar text-success me-2"></i>
|
||||||
|
<strong>تاریخ ایجاد:</strong>
|
||||||
|
<span class="ms-2">{{ instance.jcreated }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-check-circle text-info me-2"></i>
|
||||||
|
<strong>وضعیت:</strong>
|
||||||
|
<span class="ms-2 badge bg-label-primary">{{ instance.get_status_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if instance.current_step %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bx bx-step-forward text-primary me-2"></i>
|
||||||
|
<strong>مرحله فعلی:</strong>
|
||||||
|
<span class="ms-2 badge bg-label-success">{{ instance.current_step.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.description %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-start mb-2">
|
||||||
|
<i class="bx bx-note text-secondary me-2 mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<strong>توضیحات:</strong>
|
||||||
|
<p class="mb-0 ms-2 text-wrap">{{ instance.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">بستن</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -50,3 +50,57 @@ def stepper_header(instance, current_step=None):
|
||||||
}
|
}
|
||||||
|
|
||||||
# moved to _base/common/templatetags/common_tags.py
|
# moved to _base/common/templatetags/common_tags.py
|
||||||
|
|
||||||
|
@register.inclusion_tag('processes/includes/instance_info_modal.html')
|
||||||
|
def instance_info_modal(instance, modal_id=None):
|
||||||
|
"""
|
||||||
|
نمایش مدال اطلاعات کامل چاه و نماینده
|
||||||
|
|
||||||
|
استفاده:
|
||||||
|
{% load processes_tags %}
|
||||||
|
{% instance_info_modal instance %}
|
||||||
|
|
||||||
|
یا با modal_id سفارشی:
|
||||||
|
{% instance_info_modal instance "myCustomModal" %}
|
||||||
|
"""
|
||||||
|
if not isinstance(instance, ProcessInstance):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not modal_id:
|
||||||
|
modal_id = f"instanceInfoModal_{instance.id}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'instance': instance,
|
||||||
|
'modal_id': modal_id,
|
||||||
|
'well': instance.well,
|
||||||
|
'representative': instance.representative,
|
||||||
|
}
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def instance_info(instance, modal_id=None):
|
||||||
|
"""
|
||||||
|
آیکون info برای نمایش مدال اطلاعات
|
||||||
|
|
||||||
|
استفاده:
|
||||||
|
{% load processes_tags %}
|
||||||
|
نام کاربر: {{ user.name }} {% instance_info_icon instance %}
|
||||||
|
|
||||||
|
یا با modal_id سفارشی:
|
||||||
|
{% instance_info_icon instance "myCustomModal" %}
|
||||||
|
"""
|
||||||
|
if not isinstance(instance, ProcessInstance):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if not modal_id:
|
||||||
|
modal_id = f"instanceInfoModal_{instance.id}"
|
||||||
|
|
||||||
|
html = f'''
|
||||||
|
اشتراک آب: {instance.well.water_subscription_number }
|
||||||
|
| نماینده: {instance.representative.profile.national_code }
|
||||||
|
<i class="bx bx-info-circle text-muted ms-1"
|
||||||
|
style="cursor: pointer; font-size: 14px;"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#{modal_id}"
|
||||||
|
title="اطلاعات کامل چاه و نماینده"></i>
|
||||||
|
'''
|
||||||
|
return mark_safe(html)
|
||||||
|
|
|
@ -237,6 +237,7 @@ def create_request_with_entities(request):
|
||||||
well=well,
|
well=well,
|
||||||
representative=representative_user,
|
representative=representative_user,
|
||||||
requester=request.user,
|
requester=request.user,
|
||||||
|
broker=request.user.profile.broker if request.user.profile else None,
|
||||||
status='pending',
|
status='pending',
|
||||||
priority='medium',
|
priority='medium',
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue