fix date in payment and imporove contract page.
This commit is contained in:
parent
af40e169ae
commit
204b0aa48e
14 changed files with 295 additions and 74 deletions
|
@ -33,7 +33,7 @@ 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', 'broker']
|
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', 'broker']
|
list_filter = ['is_active', 'broker']
|
||||||
|
|
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='نام دارنده کارت'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -204,6 +204,12 @@ class Company(NameSlugModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='شماره تماس'
|
verbose_name='شماره تماس'
|
||||||
)
|
)
|
||||||
|
registration_number = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='شماره ثبت شرکت'
|
||||||
|
)
|
||||||
broker = models.OneToOneField(
|
broker = models.OneToOneField(
|
||||||
Broker,
|
Broker,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -238,6 +244,12 @@ class Company(NameSlugModel):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
card_holder_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="نام دارنده کارت",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
sheba_number = models.CharField(
|
sheba_number = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -372,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(){
|
||||||
|
|
|
@ -164,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>
|
||||||
|
@ -359,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
|
||||||
|
@ -422,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -323,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>
|
||||||
|
|
|
@ -149,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>
|
||||||
|
|
|
@ -172,18 +172,24 @@ def create_quote(request, instance_id, step_id):
|
||||||
quote.status = 'draft'
|
quote.status = 'draft'
|
||||||
quote.save(update_fields=['status'])
|
quote.save(update_fields=['status'])
|
||||||
|
|
||||||
if next_step:
|
# Reset ALL subsequent completed steps to in_progress
|
||||||
next_step_instance = instance.step_instances.filter(step=next_step).first()
|
subsequent_steps = instance.process.steps.filter(order__gt=step.order)
|
||||||
if next_step_instance and next_step_instance.status == 'completed':
|
for subsequent_step in subsequent_steps:
|
||||||
next_step_instance.status = 'in_progress'
|
subsequent_step_instance = instance.step_instances.filter(step=subsequent_step).first()
|
||||||
next_step_instance.completed_at = None
|
if subsequent_step_instance and subsequent_step_instance.status == 'completed':
|
||||||
next_step_instance.save(update_fields=['status', 'completed_at'])
|
# 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
|
# Clear previous approvals if the step requires re-approval
|
||||||
try:
|
try:
|
||||||
next_step_instance.approvals.all().delete()
|
subsequent_step_instance.approvals.all().delete()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Set current step to the next step
|
||||||
|
if next_step:
|
||||||
instance.current_step = next_step
|
instance.current_step = next_step
|
||||||
instance.save(update_fields=['current_step'])
|
instance.save(update_fields=['current_step'])
|
||||||
|
|
||||||
|
@ -524,6 +530,26 @@ def add_quote_payment(request, instance_id, step_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
|
# If current step is ahead of this step, reset it back to this step
|
||||||
try:
|
try:
|
||||||
if instance.current_step and instance.current_step.order > step.order:
|
if instance.current_step and instance.current_step.order > step.order:
|
||||||
|
@ -572,6 +598,26 @@ 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
|
# If current step is ahead of this step, reset it back to this step
|
||||||
try:
|
try:
|
||||||
if instance.current_step and instance.current_step.order > step.order:
|
if instance.current_step and instance.current_step.order > step.order:
|
||||||
|
@ -1000,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]),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue