add scope to filter data

This commit is contained in:
aminhashemi92 2025-09-13 12:08:50 +03:30
parent 394546dc67
commit e9dec3292c
13 changed files with 386 additions and 36 deletions

View file

@ -8,7 +8,9 @@ from django import forms
from django.contrib.auth.decorators import login_required
from accounts.models import Profile
from accounts.forms import CustomerForm
from processes.utils import scope_customers_queryset
from common.consts import UserRoles
from common.decorators import allowed_roles
# Create your views here.
@ -35,9 +37,11 @@ def dashboard(request):
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def customer_list(request):
# Get all profiles that have customer role
customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
base = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
customers = scope_customers_queryset(request.user, base)
form = CustomerForm()
return render(request, "accounts/customer_list.html", {
@ -47,6 +51,8 @@ def customer_list(request):
@require_POST
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def add_customer_ajax(request):
"""AJAX endpoint for adding customers"""
form = CustomerForm(request.POST, request.FILES)
@ -85,6 +91,8 @@ def add_customer_ajax(request):
@require_POST
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def edit_customer_ajax(request, customer_id):
customer = get_object_or_404(Profile, id=customer_id)
form = CustomerForm(request.POST, request.FILES, instance=customer)
@ -122,6 +130,7 @@ def edit_customer_ajax(request, customer_id):
})
@require_GET
@login_required
def get_customer_data(request, customer_id):
customer = get_object_or_404(Profile, id=customer_id)
@ -162,6 +171,7 @@ def get_customer_data(request, customer_id):
})
@login_required
def logout_view(request):
"""Log out current user and redirect to login page."""
logout(request)

View file

@ -12,6 +12,7 @@ from .models import CertificateTemplate, CertificateInstance
from common.consts import UserRoles
from _helpers.jalali import Gregorian
from processes.utils import get_scoped_instance_or_404
def _to_jalali(date_obj):
@ -46,7 +47,7 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance):
@login_required
def certificate_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
# Ensure all previous steps are completed and invoice settled
prior_steps = instance.process.steps.filter(order__lt=instance.current_step.order if instance.current_step else 9999)
@ -128,7 +129,7 @@ def certificate_step(request, instance_id, step_id):
@login_required
def certificate_print(request, instance_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first()
template = cert.template if cert else None
return render(request, 'certificates/print.html', {

View file

@ -3,7 +3,7 @@ from functools import wraps
from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect
from extensions.consts import UserRoles
from common.consts import UserRoles
def require_ajax(view_func):

View file

@ -11,6 +11,7 @@ from .models import ContractTemplate, ContractInstance
from invoices.models import Invoice, Quote
from _helpers.utils import jalali_converter2
from django.http import JsonResponse
from processes.utils import get_scoped_instance_or_404
def build_contract_context(instance: ProcessInstance) -> dict:
@ -52,7 +53,7 @@ def build_contract_context(instance: ProcessInstance) -> dict:
@login_required
def contract_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
# Resolve step navigation
step = get_object_or_404(instance.process.steps, id=step_id)
previous_step = instance.process.steps.filter(order__lt=step.order).last()
@ -117,7 +118,7 @@ def contract_step(request, instance_id, step_id):
@login_required
def contract_print(request, instance_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
contract = get_object_or_404(ContractInstance, process_instance=instance)
return render(request, 'contracts/contract_print.html', {
'instance': instance,

Binary file not shown.

View file

@ -10,10 +10,11 @@ from accounts.models import Role
from invoices.models import Item, Quote, QuoteItem
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
from decimal import Decimal, InvalidOperation
from processes.utils import get_scoped_instance_or_404
@login_required
def installation_assign_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
previous_step = instance.process.steps.filter(order__lt=step.order).last()
next_step = instance.process.steps.filter(order__gt=step.order).first()
@ -104,7 +105,7 @@ def create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
@login_required
def installation_report_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
previous_step = instance.process.steps.filter(order__lt=step.order).last()

View file

@ -14,11 +14,15 @@ from accounts.models import Role
from common.consts import UserRoles
from .models import Item, Quote, QuoteItem, Payment, Invoice, InvoiceItem
from installations.models import InstallationReport, InstallationItemChange
from processes.utils import get_scoped_instance_or_404
@login_required
def quote_step(request, instance_id, step_id):
"""مرحله انتخاب اقلام و ساخت پیش‌فاکتور"""
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
# Enforce scoped access to prevent URL tampering
instance = get_object_or_404(
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
id=instance_id
@ -68,7 +72,7 @@ def quote_step(request, instance_id, step_id):
@login_required
def create_quote(request, instance_id, step_id):
"""ساخت/بروزرسانی پیش‌فاکتور از اقلام انتخابی"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
# enforce permission: only BROKER can create/update quote
profile = getattr(request.user, 'profile', None)
@ -219,6 +223,9 @@ def create_quote(request, instance_id, step_id):
@login_required
def quote_preview_step(request, instance_id, step_id):
"""مرحله صدور پیش‌فاکتور - نمایش و تایید فاکتور"""
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
instance = get_object_or_404(
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
@ -261,7 +268,7 @@ def quote_preview_step(request, instance_id, step_id):
@login_required
def quote_print(request, instance_id):
"""صفحه پرینت پیش‌فاکتور"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
quote = get_object_or_404(Quote, process_instance=instance)
return render(request, 'invoices/quote_print.html', {
@ -274,7 +281,7 @@ def quote_print(request, instance_id):
@login_required
def approve_quote(request, instance_id, step_id):
"""تایید پیش‌فاکتور و انتقال به مرحله بعدی"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
quote = get_object_or_404(Quote, process_instance=instance)
# enforce permission: only BROKER can approve
@ -316,6 +323,9 @@ def approve_quote(request, instance_id, step_id):
@login_required
def quote_payment_step(request, instance_id, step_id):
"""مرحله سوم: ثبت فیش‌های واریزی پیش‌فاکتور"""
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
instance = get_object_or_404(
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
id=instance_id
@ -449,7 +459,7 @@ def quote_payment_step(request, instance_id, step_id):
@login_required
def add_quote_payment(request, instance_id, step_id):
"""افزودن فیش واریزی جدید برای پیش‌فاکتور"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, 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.get_or_create(
@ -564,7 +574,7 @@ def add_quote_payment(request, instance_id, step_id):
@require_POST
@login_required
def delete_quote_payment(request, instance_id, step_id, payment_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, 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()
@ -632,6 +642,9 @@ def delete_quote_payment(request, instance_id, step_id, payment_id):
@login_required
def final_invoice_step(request, instance_id, step_id):
"""تجمیع اقلام پیش‌فاکتور با تغییرات نصب و صدور فاکتور نهایی"""
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
instance = get_object_or_404(
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
id=instance_id
@ -770,7 +783,7 @@ def final_invoice_step(request, instance_id, step_id):
@login_required
def final_invoice_print(request, instance_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
items = invoice.items.select_related('item').filter(is_deleted=False).all()
return render(request, 'invoices/final_invoice_print.html', {
@ -783,7 +796,7 @@ def final_invoice_print(request, instance_id):
@require_POST
@login_required
def approve_final_invoice(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
# only MANAGER can approve
@ -811,7 +824,7 @@ def approve_final_invoice(request, instance_id, step_id):
@login_required
def add_special_charge(request, instance_id, step_id):
"""افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی به‌صورت آیتم جداگانه"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
# only MANAGER can add special charges
try:
@ -848,7 +861,7 @@ def add_special_charge(request, instance_id, step_id):
@require_POST
@login_required
def delete_special_charge(request, instance_id, step_id, item_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
# only MANAGER can delete special charges
try:
@ -870,7 +883,7 @@ def delete_special_charge(request, instance_id, step_id, item_id):
@login_required
def final_settlement_step(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
if not instance.can_access_step(step):
@ -976,7 +989,7 @@ def final_settlement_step(request, instance_id, step_id):
@require_POST
@login_required
def add_final_payment(request, instance_id, step_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
# Only BROKER can add final settlement payments
@ -1093,7 +1106,7 @@ def add_final_payment(request, instance_id, step_id):
@require_POST
@login_required
def delete_final_payment(request, instance_id, step_id, payment_id):
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
step = get_object_or_404(instance.process.steps, id=step_id)
invoice = get_object_or_404(Invoice, process_instance=instance)
payment = get_object_or_404(Payment, id=payment_id, invoice=invoice)

View file

@ -1,5 +1,6 @@
{% extends '_base.html' %}
{% load static %}
{% load accounts_tags %}
{% block sidebar %}
{% include 'sidebars/admin.html' %}
@ -43,10 +44,12 @@
</span>
</span>
</button>
{% if request.user|is_broker %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#requestModal">
<i class="bx bx-plus me-1"></i>
درخواست جدید
</button>
{% endif %}
</div>
</div>
</div>
@ -132,6 +135,91 @@
</div>
</div>
{% if access_denied %}
<div class="alert alert-warning d-flex align-items-center mb-3" role="alert">
<i class="bx bx-info-circle me-2"></i>
<div>شما به این بخش دسترسی ندارید.</div>
</div>
{% endif %}
<div class="card mb-3">
<div class="card-body">
<form method="get" class="row g-2 align-items-end">
<div class="col-sm-6 col-md-3">
<label class="form-label">وضعیت درخواست</label>
<select class="form-select" name="status">
<option value="">همه</option>
{% for val, label in status_choices %}
<option value="{{ val }}" {% if filter_status == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}@require_POST
@login_required
def delete_request(request, instance_id):
"""حذف درخواست"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
# Only BROKER can delete requests and only within their scope
try:
profile = getattr(request.user, 'profile', None)
if not (profile and profile.has_role(UserRoles.BROKER)):
return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
# Enforce ownership by broker (prevent deleting others' requests)
if instance.broker_id and profile.broker and instance.broker_id != profile.broker.id:
return JsonResponse({'success': False, 'message': 'شما مجاز به حذف این درخواست نیستید'}, status=403)
except Exception:
return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
code = instance.code
if instance.status == 'completed':
return JsonResponse({
'success': False,
'message': 'درخواست تکمیل شده نمی‌تواند حذف شود'
})
instance.delete()
return JsonResponse({
'success': True,
'message': f'درخواست {code} با موفقیت حذف شد'
})
</select>
</div>
<div class="col-sm-6 col-md-3">
<label class="form-label">امور</label>
<select class="form-select" name="affairs">
<option value="">همه</option>
{% for a in affairs_list %}
<option value="{{ a.id }}" {% if filter_affairs|default:''|stringformat:'s' == a.id|stringformat:'s' %}selected{% endif %}>{{ a.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-3">
<label class="form-label">کارگزار</label>
<select class="form-select" name="broker">
<option value="">همه</option>
{% for b in brokers_list %}
<option value="{{ b.id }}" {% if filter_broker|default:''|stringformat:'s' == b.id|stringformat:'s' %}selected{% endif %}>{{ b.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-3">
<label class="form-label">مرحله فعلی</label>
<select class="form-select" name="step">
<option value="">همه</option>
{% for s in steps_list %}
<option value="{{ s.id }}" {% if filter_step|default:''|stringformat:'s' == s.id|stringformat:'s' %}selected{% endif %}>{{ s.process.name }} - {{ s.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 d-flex gap-2 justify-content-end mt-3">
<button type="submit" class="btn btn-primary">
<i class="bx bx-filter-alt me-1"></i>
اعمال فیلتر
</button>
<a href="?" class="btn btn-outline-secondary">
<i class="bx bx-x me-1"></i>
حذف فیلتر
</a>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-datatable table-responsive">
<table id="requests-table" class="datatables-basic table border-top">
@ -178,7 +266,7 @@
</div>
</td>
<td>{{ item.instance.get_status_display_with_color|safe }}</td>
<td>{{ item.instance.jcreated }}</td>
<td>{{ item.instance.jcreated_date }}</td>
<td>
<div class="d-inline-block">
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
@ -196,19 +284,31 @@
</a>
{% endif %}
</li>
{% if request.user|is_broker %}
<div class="dropdown-divider"></div>
<li>
<a href="#" class="dropdown-item text-danger" data-instance-id="{{ item.instance.id }}" data-instance-code="{{ item.instance.code }}" onclick="deleteRequest(this.getAttribute('data-instance-id'), this.getAttribute('data-instance-code'))">
<i class="bx bx-trash me-1"></i>حذف
</a>
</li>
{% endif %}
</ul>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center text-muted">موردی ثبت نشده است</td>
<td class="text-center text-muted">موردی ثبت نشده است</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,6 +1,7 @@
from django import template
from django.utils.safestring import mark_safe
from ..models import ProcessInstance, StepInstance
from ..utils import count_incomplete_instances
register = template.Library()
@ -104,3 +105,8 @@ def instance_info(instance, modal_id=None):
title="اطلاعات کامل چاه و نماینده"></i>
'''
return mark_safe(html)
@register.simple_tag
def incomplete_requests_count(user):
return count_incomplete_instances(user)

118
processes/utils.py Normal file
View file

@ -0,0 +1,118 @@
from django.shortcuts import get_object_or_404
from .models import ProcessInstance
from common.consts import UserRoles
def scope_instances_queryset(user, queryset=None):
"""Return a queryset of ProcessInstance scoped by the user's role.
If no profile/role, returns an empty queryset.
"""
qs = queryset if queryset is not None else ProcessInstance.objects.all()
profile = getattr(user, 'profile', None)
if not profile:
return qs.none()
try:
if profile.has_role(UserRoles.INSTALLER):
# Only instances assigned to this installer
from installations.models import InstallationAssignment
assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
return qs.filter(id__in=assign_ids)
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
return qs.filter(broker__affairs__county=profile.county)
if profile.has_role(UserRoles.ADMIN):
return qs
# if profile.has_role(UserRoles.WATER_RESOURCE_MANAGER) or profile.has_role(UserRoles.HEADQUARTER):
# return qs.filter(well__county=profile.county)
# Fallback: no special scope
# return qs
except Exception:
return qs.none()
def count_incomplete_instances(user):
"""Count non-completed, non-deleted requests within the user's scope."""
base = ProcessInstance.objects.select_related('well').filter(is_deleted=False).exclude(status='completed')
return scope_instances_queryset(user, base).count()
def user_can_access_instance(user, instance: ProcessInstance) -> bool:
"""Check if user can access a specific instance based on scoping rules."""
try:
scoped = scope_instances_queryset(user, ProcessInstance.objects.filter(id=instance.id))
return scoped.exists()
except Exception:
return False
def get_scoped_instance_or_404(request, instance_id: int) -> ProcessInstance:
"""Return instance only if it's within the user's scope; otherwise 404.
Use this in any view receiving instance_id from URL to prevent URL tampering.
"""
base = ProcessInstance.objects.filter(is_deleted=False)
qs = scope_instances_queryset(request.user, base)
return get_object_or_404(qs, id=instance_id)
def scope_wells_queryset(user, queryset=None):
"""Return a queryset of Well scoped by the user's role (parity with instances)."""
try:
from wells.models import Well
qs = queryset if queryset is not None else Well.objects.all()
profile = getattr(user, 'profile', None)
if not profile:
return qs.none()
if profile.has_role(UserRoles.ADMIN):
return qs
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
return qs.filter(broker__affairs__county=profile.county)
if profile.has_role(UserRoles.INSTALLER):
# Wells that have instances assigned to this installer
from installations.models import InstallationAssignment
assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
inst_qs = ProcessInstance.objects.filter(id__in=assign_ids)
return qs.filter(process_instances__in=inst_qs).distinct()
# Fallback
return qs.none()
except Exception:
return qs.none() if 'qs' in locals() else []
def scope_customers_queryset(user, queryset=None):
"""Return a queryset of customer Profiles scoped by user's role.
Assumes queryset is Profiles already filtered to customers, otherwise we filter here.
"""
try:
from accounts.models import Profile
qs = queryset if queryset is not None else Profile.objects.all()
# Ensure we're only looking at customer profiles
from common.consts import UserRoles as UR
qs = qs.filter(roles__slug=UR.CUSTOMER.value, is_deleted=False)
profile = getattr(user, 'profile', None)
if not profile:
return qs.none()
if profile.has_role(UserRoles.ADMIN):
return qs
if profile.has_role(UserRoles.BROKER):
return qs.filter(broker=profile.broker)
if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER):
return qs.filter(county=profile.county)
if profile.has_role(UserRoles.INSTALLER):
# Customers that are representatives of instances assigned to this installer
from installations.models import InstallationAssignment
assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True)
rep_ids = ProcessInstance.objects.filter(id__in=assign_ids).values_list('representative', flat=True)
return qs.filter(user_id__in=rep_ids)
# Fallback
return qs.none()
except Exception:
return qs.none() if 'qs' in locals() else []

View file

@ -7,19 +7,62 @@ from django.http import JsonResponse
from django.views.decorators.http import require_POST, require_GET
from django.db import transaction
from django.contrib.auth import get_user_model
from .models import Process, ProcessInstance, StepInstance
from .models import Process, ProcessInstance, StepInstance, ProcessStep
from .utils import scope_instances_queryset, get_scoped_instance_or_404
from installations.models import InstallationAssignment
from wells.models import Well
from accounts.models import Profile
from accounts.models import Profile, Broker
from locations.models import Affairs
from accounts.forms import CustomerForm
from wells.forms import WellForm
from wells.models import WaterMeterManufacturer
from common.consts import UserRoles
@login_required
def request_list(request):
"""نمایش لیست درخواست‌ها با جدول و مدال ایجاد"""
instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created')
instances = ProcessInstance.objects.select_related('well', 'representative', 'requester', 'broker', 'current_step', 'process').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created')
access_denied = False
# filter by roles (scoped queryset)
try:
instances = scope_instances_queryset(request.user, instances)
if not instances.exists() and not getattr(request.user, 'profile', None):
access_denied = True
instances = instances.none()
except Exception:
access_denied = True
instances = instances.none()
# Filters
status_q = (request.GET.get('status') or '').strip()
affairs_q = (request.GET.get('affairs') or '').strip()
broker_q = (request.GET.get('broker') or '').strip()
step_q = (request.GET.get('step') or '').strip()
if status_q:
instances = instances.filter(status=status_q)
if affairs_q:
try:
instances = instances.filter(well__affairs_id=int(affairs_q))
except Exception:
pass
if broker_q:
try:
instances = instances.filter(broker_id=int(broker_q))
except Exception:
pass
if step_q:
try:
instances = instances.filter(current_step_id=int(step_q))
except Exception:
pass
processes = Process.objects.filter(is_active=True)
status_choices = list(ProcessInstance.STATUS_CHOICES)
affairs_list = Affairs.objects.all().order_by('name')
brokers_list = Broker.objects.all().order_by('name')
steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order')
manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
# Calculate progress for each instance
@ -52,6 +95,16 @@ def request_list(request):
'completed_count': completed_count,
'in_progress_count': in_progress_count,
'pending_count': pending_count,
# filter context
'status_choices': status_choices,
'affairs_list': affairs_list,
'brokers_list': brokers_list,
'steps_list': steps_list,
'filter_status': status_q,
'filter_affairs': affairs_q,
'filter_broker': broker_q,
'filter_step': step_q,
'access_denied': access_denied,
})
@ -125,6 +178,13 @@ def lookup_representative_by_national_code(request):
def create_request_with_entities(request):
"""ایجاد/به‌روزرسانی چاه و نماینده و سپس ایجاد درخواست"""
User = get_user_model()
# Only BROKER can create requests
try:
if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)):
return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403)
except Exception:
return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403)
process_id = request.POST.get('process')
process = Process.objects.get(id=process_id)
description = request.POST.get('description', '')
@ -230,6 +290,14 @@ def create_request_with_entities(request):
well.broker = current_profile.broker
well.save()
# Ensure no active (non-deleted, non-completed) request exists for this well
try:
active_exists = ProcessInstance.objects.filter(well=well, is_deleted=False).exclude(status='completed').exists()
if active_exists:
return JsonResponse({'ok': False, 'error': 'برای این چاه یک درخواست جاری وجود دارد. ابتدا آن را تکمیل یا حذف کنید.'}, status=400)
except Exception:
return JsonResponse({'ok': False, 'error': 'خطا در بررسی وضعیت درخواست‌های قبلی این چاه'}, status=400)
# Create request instance
instance = ProcessInstance.objects.create(
process=process,
@ -261,7 +329,17 @@ def create_request_with_entities(request):
@login_required
def delete_request(request, instance_id):
"""حذف درخواست"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
instance = get_scoped_instance_or_404(request, instance_id)
# Only BROKER can delete requests and only within their scope
try:
profile = getattr(request.user, 'profile', None)
if not (profile and profile.has_role(UserRoles.BROKER)):
return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
# Enforce ownership by broker (prevent deleting others' requests)
if instance.broker_id and profile.broker and instance.broker_id != profile.broker.id:
return JsonResponse({'success': False, 'message': 'شما مجاز به حذف این درخواست نیستید'}, status=403)
except Exception:
return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403)
code = instance.code
if instance.status == 'completed':
return JsonResponse({
@ -278,10 +356,10 @@ def delete_request(request, instance_id):
@login_required
def step_detail(request, instance_id, step_id):
"""نمایش جزئیات مرحله خاص"""
instance = get_object_or_404(
ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'),
id=instance_id
)
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
# Prefetch for performance
instance = ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile').get(id=instance.id)
step = get_object_or_404(instance.process.steps, id=step_id)
# If the request is already completed, redirect to read-only summary page
if instance.status == 'completed':
@ -339,7 +417,8 @@ def step_detail(request, instance_id, step_id):
@login_required
def instance_steps(request, instance_id):
"""هدایت به مرحله فعلی instance"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
if not instance.current_step:
# اگر مرحله فعلی تعریف نشده، به اولین مرحله برو
@ -361,6 +440,9 @@ def instance_steps(request, instance_id):
@login_required
def instance_summary(request, instance_id):
"""نمای خلاصهٔ فقط‌خواندنی برای درخواست‌های تکمیل‌شده."""
# Enforce scoped access to prevent URL tampering
instance = get_scoped_instance_or_404(request, instance_id)
instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id)
# Only show for completed requests; otherwise route to steps
if instance.status != 'completed':

View file

@ -1,4 +1,5 @@
{% load static %}
{% load accounts_tags %}
<!-- Menu -->
<aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme">
@ -108,9 +109,12 @@
<a href="{% url 'processes:request_list' %}" class="menu-link">
<i class="menu-icon tf-icons bx bx-user"></i>
<div class="text-truncate">درخواست‌ها</div>
{% load processes_tags %}
<span class="badge badge-center rounded-pill bg-danger ms-auto">{% incomplete_requests_count request.user %}</span>
</a>
</li>
{% if request.user|is_admin or request.user|is_broker or request.user|is_manager or request.user|is_accountant %}
<!-- Customers -->
<li class="menu-header small text-uppercase">
<span class="menu-header-text">مشترک‌ها</span>
@ -131,11 +135,11 @@
<div class="text-truncate">چاه‌ها</div>
</a>
</li>
{% endif %}
<!-- Apps & Pages -->
<li class="menu-header small text-uppercase">
<li class="menu-header small text-uppercase d-none">
<span class="menu-header-text">گزارش‌ها</span>
</li>

View file

@ -7,17 +7,23 @@ from django.contrib import messages
from django import forms
from .models import Well, WaterMeterManufacturer
from .forms import WellForm, WaterMeterManufacturerForm
from django.contrib.auth.decorators import login_required
from common.decorators import allowed_roles
from common.consts import UserRoles
from processes.utils import scope_wells_queryset
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def well_list(request):
"""نمایش لیست چاه‌ها"""
wells = Well.objects.select_related(
base = Well.objects.select_related(
'representative',
'water_meter_manufacturer',
'affairs',
'county',
'broker'
).filter(is_deleted=False)
wells = scope_wells_queryset(request.user, base)
# فرم برای افزودن چاه جدید
form = WellForm()
@ -31,6 +37,8 @@ def well_list(request):
@require_POST
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def add_well_ajax(request):
"""AJAX endpoint for adding wells"""
try:
@ -87,6 +95,8 @@ def add_well_ajax(request):
@require_POST
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def edit_well_ajax(request, well_id):
"""AJAX endpoint for editing wells"""
well = get_object_or_404(Well, id=well_id)
@ -141,6 +151,8 @@ def edit_well_ajax(request, well_id):
@require_POST
@login_required
@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT])
def delete_well(request, well_id):
"""حذف چاه"""
well = get_object_or_404(Well, id=well_id)
@ -154,6 +166,7 @@ def delete_well(request, well_id):
@require_GET
@login_required
def get_well_data(request, well_id):
"""دریافت اطلاعات چاه برای ویرایش"""
well = get_object_or_404(Well, id=well_id)
@ -183,6 +196,7 @@ def get_well_data(request, well_id):
@require_POST
@login_required
def create_water_meter_manufacturer(request):
"""ایجاد شرکت سازنده کنتور آب جدید"""
form = WaterMeterManufacturerForm(request.POST)