Add confirmation and summary

This commit is contained in:
aminhashemi92 2025-09-05 13:35:33 +03:30
parent 9b3973805e
commit 35799b7754
25 changed files with 1419 additions and 265 deletions

View file

@ -1,6 +1,7 @@
{% extends '_base.html' %}
{% load static %}
{% load processes_tags %}
{% load common_tags %}
{% load humanize %}
{% block sidebar %}
@ -41,12 +42,15 @@
<div class="bs-stepper-content">
{% if show_denied_msg %}
<div class="alert alert-warning mb-3">شما اجازه تعیین نصاب را ندارید.</div>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">نصاب</label>
<select name="installer_id" class="form-select" required>
<select name="installer_id" class="form-select" {% if read_only %}disabled{% endif %} required>
<option value="">انتخاب کنید...</option>
{% for p in installers %}
<option value="{{ p.user.id }}" {% if assignment.installer and p.user.id == assignment.installer.id %}selected{% endif %}>{{ p.user.get_full_name }} ({{ p.user.username }})</option>
@ -55,17 +59,39 @@
</div>
<div class="col-md-6">
<label class="form-label">تاریخ مراجعه نصاب</label>
<input type="text" id="id_scheduled_date_display" class="form-control" placeholder="انتخاب تاریخ" readonly required value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y/m/d' }}{% endif %}">
<input type="text" id="id_scheduled_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if read_only %}disabled{% endif %} readonly required value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y/m/d' }}{% endif %}">
<input type="hidden" id="id_scheduled_date" name="scheduled_date" value="{% if assignment.scheduled_date %}{{ assignment.scheduled_date|date:'Y-m-d' }}{% endif %}">
</div>
</div>
{% if assignment.assigned_by or assignment.installer %}
<div class="mt-3 border rounded p-3 bg-light">
<div class="row g-2">
{% if assignment.assigned_by %}
<div class="col-12 col-md-6">
<div class="small text-muted">تعیین‌کننده نصاب</div>
<div>{{ assignment.assigned_by.get_full_name|default:assignment.assigned_by.username }} <span class="text-muted">({{ assignment.assigned_by.username }})</span></div>
</div>
{% endif %}
{% if assignment.updated %}
<div class="col-12 col-md-6">
<div class="small text-muted">تاریخ ثبت/ویرایش</div>
<div>{{ assignment.updated|to_jalali }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between mt-4">
{% if previous_step %}
<a href="{% url 'processes:step_detail' instance.id previous_step.id %}" class="btn btn-label-secondary">قبلی</a>
{% else %}
<span></span>
{% endif %}
<button class="btn btn-primary" type="submit">ثبت و ادامه</button>
{% if is_manager %}
<button class="btn btn-primary" type="submit">ثبت و ادامه</button>
{% else %}
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-primary">بعدی</a>
{% endif %}
</div>
</form>
</div>

View file

@ -2,6 +2,7 @@
{% load static %}
{% load processes_tags %}
{% load common_tags %}
{% load accounts_tags %}
{% load humanize %}
{% block sidebar %}
@ -41,13 +42,31 @@
{% stepper_header instance step %}
<div class="bs-stepper-content">
{% if report and not edit_mode %}
<div class="card mb-3 border">
<div class="card-header d-flex justify-content-end">
<a href="?edit=1" class="btn btn-primary">ویرایش گزارش نصب</a>
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
{% if request.user|is_installer %}
<a href="?edit=1" class="btn btn-primary">ویرایش گزارش نصب</a>
{% else %}
<button type="button" class="btn btn-primary" disabled>ویرایش گزارش نصب</button>
{% endif %}
{% if user_can_approve %}
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal" {% if step_instance and step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %}
<div class="alert alert-danger d-flex align-items-start" role="alert">
<i class="bx bx-error-circle me-2"></i>
<div>
<div><strong>این گزارش رد شده است.</strong></div>
<div class="mt-1 small">علت رد: {{ step_instance.get_latest_rejection.reason }}</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
@ -67,6 +86,9 @@
</div>
{% endif %}
<hr>
{% if request.user|is_manager or request.user|is_admin %}
<hr>
{% endif %}
<h6>عکس‌ها</h6>
<div class="row">
{% for p in report.photos.all %}
@ -115,6 +137,42 @@
</div>
</div>
</div>
{% if approver_statuses %}
<div class="card border mt-2">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">وضعیت تاییدها</h6>
{% if user_can_approve %}
<div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal" {% if step_instance and step_instance.status == 'completed' %}disabled{% endif %}>تایید</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
</div>
{% endif %}
</div>
<div class="card-body py-3">
<div class="row g-2">
{% for st in approver_statuses %}
<div class="col-12 col-md-6 col-lg-4">
<div class="d-flex flex-column border rounded px-2 py-1">
<div class="d-flex align-items-center gap-2">
<span class="badge bg-light text-dark">{{ st.role.name }}</span>
{% if st.status == 'approved' %}
<span class="badge bg-success">تایید شد</span>
{% elif st.status == 'rejected' %}
<span class="badge bg-danger">رد شد</span>
{% else %}
<span class="badge bg-warning text-dark">در انتظار</span>
{% endif %}
</div>
{% if st.status == 'rejected' and st.reason %}
<div class="mt-1 small text-danger">علت: {{ st.reason }}</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Persistent nav in edit mode (outside cards) -->
<div class="d-flex justify-content-between mt-3">
{% if previous_step %}
@ -127,6 +185,9 @@
{% endif %}
</div>
{% else %}
{% if not request.user|is_installer %}
<div class="alert alert-warning">شما مجوز ثبت/ویرایش گزارش نصب را ندارید. اطلاعات به صورت فقط خواندنی نمایش داده می‌شود.</div>
{% endif %}
<form method="post" enctype="multipart/form-data" id="installation-report-form">
{% csrf_token %}
<div class="mb-3">
@ -134,40 +195,42 @@
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">تاریخ مراجعه</label>
<input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
<input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if not request.user|is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
<input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% endif %}">
</div>
<div class="col-md-3">
<label class="form-label">سریال کنتور جدید</label>
<input type="text" class="form-control" name="new_water_meter_serial">
<input type="text" class="form-control" name="new_water_meter_serial" value="{% if report and edit_mode %}{{ report.new_water_meter_serial|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
</div>
<div class="col-md-3">
<label class="form-label">شماره پلمپ</label>
<input type="text" class="form-control" name="seal_number">
<input type="text" class="form-control" name="seal_number" value="{% if report and edit_mode %}{{ report.seal_number|default_if_none:'' }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious">
<input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious" {% if not request.user|is_installer %}disabled{% endif %} {% if report and edit_mode and report.is_meter_suspicious %}checked{% endif %}>
<label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
</div>
</div>
<div class="col-md-3">
<label class="form-label">UTM X</label>
<input type="number" step="0.000001" class="form-control" name="utm_x" value="{% if instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}">
<input type="number" step="0.000001" class="form-control" name="utm_x" value="{% if report and edit_mode and report.utm_x %}{{ report.utm_x }}{% elif instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
</div>
<div class="col-md-3">
<label class="form-label">UTM Y</label>
<input type="number" step="0.000001" class="form-control" name="utm_y" value="{% if instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}">
<input type="number" step="0.000001" class="form-control" name="utm_y" value="{% if report and edit_mode and report.utm_y %}{{ report.utm_y }}{% elif instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}" {% if not request.user|is_installer %}readonly{% endif %}>
</div>
</div>
<div class="my-3">
<label class="form-label">توضیحات (اختیاری)</label>
<textarea class="form-control" rows="3" name="description"></textarea>
<textarea class="form-control" rows="3" name="description" {% if not request.user|is_installer %}readonly{% endif %}>{% if report and edit_mode %}{{ report.description|default_if_none:'' }}{% endif %}</textarea>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<label class="form-label mb-0">عکس‌ها</label>
<button type="button" class="btn btn-sm btn-outline-primary" id="btnAddPhoto"><i class="bx bx-plus"></i> افزودن عکس</button>
{% if request.user|is_installer %}
<button type="button" class="btn btn-sm btn-outline-primary" id="btnAddPhoto"><i class="bx bx-plus"></i> افزودن عکس</button>
{% endif %}
</div>
{% if report %}
<div class="row mt-2">
@ -175,7 +238,9 @@
<div class="col-6 col-md-3 mb-2" id="existing-photo-{{ p.id }}">
<div class="position-relative border rounded p-1">
<img class="img-fluid rounded" src="{{ p.image.url }}" alt="photo">
<button type="button" class="btn btn-sm btn-danger position-absolute" style="top:6px; left:6px;" onclick="markDeletePhoto({{ p.id }})" title="حذف/برگردان"><i class='bx bx-trash'></i></button>
{% if request.user|is_installer %}
<button type="button" class="btn btn-sm btn-danger position-absolute" style="top:6px; left:6px;" onclick="markDeletePhoto('{{ p.id }}')" title="حذف/برگردان"><i class="bx bx-trash"></i></button>
{% endif %}
<input type="hidden" name="del_photo_{{ p.id }}" id="del-photo-{{ p.id }}" value="0">
</div>
</div>
@ -285,7 +350,11 @@
<span></span>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" form="installation-report-form">ثبت گزارش</button>
{% if request.user|is_installer %}
<button type="submit" class="btn btn-primary" form="installation-report-form">ثبت گزارش</button>
{% else %}
<button type="button" class="btn btn-primary" disabled>ثبت گزارش</button>
{% endif %}
{% if next_step %}
<a href="{% url 'processes:step_detail' instance.id next_step.id %}" class="btn btn-success">بعدی</a>
{% endif %}
@ -298,6 +367,58 @@
</div>
</div>
</div>
<!-- Approve Modal -->
<div class="modal fade" id="approveModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="approve">
<div class="modal-header">
<h5 class="modal-title">تایید گزارش نصب</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
آیا از تایید این گزارش اطمینان دارید؟
</div>
<div class="modal-footer">
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
<button type="submit" class="btn btn-success">تایید</button>
</div>
</form>
</div>
</div>
</div>
<!-- Reject Modal -->
<div class="modal fade" id="rejectModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="reject">
<div class="modal-header">
<h5 class="modal-title">رد گزارش نصب</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label class="form-label">علت رد</label>
<textarea class="form-control" name="reject_reason" rows="3" required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">انصراف</button>
<button type="submit" class="btn btn-danger">ثبت رد</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block script %}
@ -445,4 +566,3 @@
</script>
{% endblock %}

View file

@ -5,7 +5,8 @@ from django.urls import reverse
from django.utils import timezone
from accounts.models import Profile
from common.consts import UserRoles
from processes.models import ProcessInstance, StepInstance
from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval
from accounts.models import Role
from invoices.models import Item, Quote, QuoteItem
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
from decimal import Decimal, InvalidOperation
@ -21,7 +22,18 @@ def installation_assign_step(request, instance_id, step_id):
installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value).select_related('user').all()
assignment, _ = InstallationAssignment.objects.get_or_create(process_instance=instance)
# Role flags
profile = getattr(request.user, 'profile', None)
is_manager = False
try:
is_manager = bool(profile and profile.has_role(UserRoles.MANAGER))
except Exception:
is_manager = False
if request.method == 'POST':
if not is_manager:
messages.error(request, 'شما اجازه تعیین نصاب را ندارید')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
installer_id = request.POST.get('installer_id')
scheduled_date = (request.POST.get('scheduled_date') or '').strip()
assignment.installer_id = installer_id or None
@ -43,6 +55,10 @@ def installation_assign_step(request, instance_id, step_id):
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
return redirect('processes:request_list')
# Read-only logic for non-managers
read_only = not is_manager
show_denied_msg = (not is_manager) and (assignment.installer_id is None)
return render(request, 'installations/installation_assign_step.html', {
'instance': instance,
'step': step,
@ -50,6 +66,9 @@ def installation_assign_step(request, instance_id, step_id):
'installers': installers,
'previous_step': previous_step,
'next_step': next_step,
'is_manager': is_manager,
'read_only': read_only,
'show_denied_msg': show_denied_msg,
})
@ -61,15 +80,94 @@ def installation_report_step(request, instance_id, step_id):
next_step = instance.process.steps.filter(order__gt=step.order).first()
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first()
edit_mode = True if request.GET.get('edit') == '1' else False
print("edit_mode", edit_mode)
# Only installers can enter edit mode
user_is_installer = hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.INSTALLER)
edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False
# current quote items baseline
quote = Quote.objects.filter(process_instance=instance).first()
quote_items = list(quote.items.select_related('item').all()) if quote else []
quote_price_map = {qi.item_id: qi.unit_price for qi in quote_items}
items = Item.objects.all().order_by('name')
items = Item.objects.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
# Ensure a StepInstance exists for this step
step_instance, _ = StepInstance.objects.get_or_create(
process_instance=instance,
step=step,
defaults={'status': 'in_progress'}
)
# Build approver requirements/status for UI
reqs = list(step.approver_requirements.select_related('role').all())
user_roles_qs = getattr(getattr(request.user, 'profile', None), 'roles', None)
user_roles = list(user_roles_qs.all()) if user_roles_qs is not None else []
user_can_approve = any(r.role in user_roles for r in reqs)
approvals_list = list(step_instance.approvals.select_related('role').all())
approvals_by_role = {a.role_id: a for a in approvals_list}
approver_statuses = [
{
'role': r.role,
'status': (approvals_by_role.get(r.role_id).decision if approvals_by_role.get(r.role_id) else None),
'reason': (approvals_by_role.get(r.role_id).reason if approvals_by_role.get(r.role_id) else ''),
}
for r in reqs
]
# Manager approval/rejection actions
if request.method == 'POST' and request.POST.get('action') in ['approve', 'reject']:
action = request.POST.get('action')
# find a matching approver role based on step requirements
req_roles = [req.role for req in step.approver_requirements.select_related('role').all()]
user_roles = list(getattr(getattr(request.user, 'profile', None), 'roles', Role.objects.none()).all())
matching_role = next((r for r in user_roles if r in req_roles), None)
if matching_role is None:
messages.error(request, 'شما دسترسی لازم برای این عملیات را ندارید.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
if not existing_report:
messages.error(request, 'گزارش برای تایید/رد وجود ندارد.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
if action == 'approve':
existing_report.approved = True
existing_report.save()
StepApproval.objects.update_or_create(
step_instance=step_instance,
role=matching_role,
defaults={'approved_by': request.user, 'decision': 'approved', 'reason': ''}
)
if step_instance.is_fully_approved():
step_instance.status = 'completed'
step_instance.completed_at = timezone.now()
step_instance.save()
if next_step:
instance.current_step = next_step
instance.save()
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
return redirect('processes:request_list')
messages.success(request, 'تایید شما ثبت شد. منتظر تایید سایر نقش‌ها.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
if action == 'reject':
reason = (request.POST.get('reject_reason') or '').strip()
if not reason:
messages.error(request, 'لطفاً علت رد شدن را وارد کنید.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
StepApproval.objects.update_or_create(
step_instance=step_instance,
role=matching_role,
defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason}
)
StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason)
existing_report.approved = False
existing_report.save()
messages.success(request, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
if request.method == 'POST':
# Only installers can submit or edit reports (non-approval actions)
if request.POST.get('action') not in ['approve', 'reject'] and not user_is_installer:
messages.error(request, 'شما مجوز ثبت/ویرایش گزارش نصب را ندارید')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
description = (request.POST.get('description') or '').strip()
visited_date = (request.POST.get('visited_date') or '').strip()
if '/' in visited_date:
@ -134,6 +232,7 @@ def installation_report_step(request, instance_id, step_id):
report.is_meter_suspicious = is_suspicious
report.utm_x = utm_x
report.utm_y = utm_y
report.approved = False # back to awaiting approval after edits
report.save()
# delete selected existing photos
for key, val in request.POST.items():
@ -211,18 +310,17 @@ def installation_report_step(request, instance_id, step_id):
total_price=total,
)
# complete step
StepInstance.objects.update_or_create(
process_instance=instance,
step=step,
defaults={'status': 'completed', 'completed_at': timezone.now()}
)
# After installer submits/edits, set step back to in_progress and clear approvals
step_instance.status = 'in_progress'
step_instance.completed_at = None
step_instance.save()
try:
step_instance.approvals.all().delete()
except Exception:
pass
if next_step:
instance.current_step = next_step
instance.save()
return redirect('processes:step_detail', instance_id=instance.id, step_id=next_step.id)
return redirect('processes:request_list')
messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
# Build prefill maps from existing report changes
removed_ids = set()
@ -250,6 +348,9 @@ def installation_report_step(request, instance_id, step_id):
'added_map': added_map,
'previous_step': previous_step,
'next_step': next_step,
'step_instance': step_instance,
'approver_statuses': approver_statuses,
'user_can_approve': user_can_approve,
})