shafafiyat/installations/views.py
2025-10-03 21:56:25 +03:30

455 lines
20 KiB
Python

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.urls import reverse
from django.utils import timezone
from django.core.exceptions import ValidationError
from accounts.models import Profile
from common.consts import UserRoles
from processes.models import ProcessInstance, StepInstance, StepRejection, StepApproval
from accounts.models import Role
from invoices.models import Item, Quote, QuoteItem
from wells.models import WaterMeterManufacturer
from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange
from .forms import InstallationReportForm
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_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()
# Installers list (profiles that have installer role)
installers = Profile.objects.filter(roles__slug=UserRoles.INSTALLER.value, county=instance.well.county).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
if scheduled_date:
assignment.scheduled_date = scheduled_date.replace('/', '-')
assignment.assigned_by = request.user
assignment.save()
# complete step
StepInstance.objects.update_or_create(
process_instance=instance,
step=step,
defaults={'status': 'completed', 'completed_at': timezone.now()}
)
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')
# 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,
'assignment': assignment,
'installers': installers,
'previous_step': previous_step,
'next_step': next_step,
'is_manager': is_manager,
'read_only': read_only,
'show_denied_msg': show_denied_msg,
})
def create_item_changes_for_report(report, remove_map, add_map, quote_price_map):
"""Helper function to create item changes for a report"""
# Create remove changes
for item_id, qty in remove_map.items():
up = quote_price_map.get(item_id)
total = (up * qty) if up is not None else None
InstallationItemChange.objects.create(
report=report,
item_id=item_id,
change_type='remove',
quantity=qty,
unit_price=up,
total_price=total,
)
# Create add changes
for item_id, data in add_map.items():
unit_price = data.get('price')
qty = data.get('qty') or 1
total = (unit_price * qty) if (unit_price is not None) else None
InstallationItemChange.objects.create(
report=report,
item_id=item_id,
change_type='add',
quantity=qty,
unit_price=unit_price,
total_price=total,
)
@login_required
def installation_report_step(request, instance_id, step_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()
assignment = InstallationAssignment.objects.filter(process_instance=instance).first()
existing_report = InstallationReport.objects.filter(assignment=assignment).order_by('-created').first()
# Only the assigned installer can create/edit the report
try:
has_installer_role = bool(getattr(request.user, 'profile', None) and request.user.profile.has_role(UserRoles.INSTALLER))
except Exception:
has_installer_role = False
is_assigned_installer = bool(assignment and assignment.installer_id == request.user.id)
user_is_installer = bool(has_installer_role and is_assigned_installer)
edit_mode = True if (request.GET.get('edit') == '1' and user_is_installer) else False
# Prevent edit mode if an approved report exists
if existing_report and existing_report.approved:
edit_mode = False
# Ensure a StepInstance exists for this step
step_instance, _ = StepInstance.objects.get_or_create(
process_instance=instance,
step=step,
defaults={'status': 'in_progress'}
)
# 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.filter(is_active=True, is_special=False, is_deleted=False).order_by('name')
manufacturers = WaterMeterManufacturer.objects.all().order_by('name')
# Initialize the form
form = None
if request.method == 'POST' and request.POST.get('action') not in ['approve', 'reject']:
# Handle form submission for report creation/editing
if not user_is_installer:
messages.error(request, 'شما مجوز ثبت/ویرایش گزارش نصب را ندارید')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
# Block editing approved reports
if existing_report and existing_report.approved:
messages.error(request, 'این گزارش قبلا تایید شده و قابل ویرایش نیست')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
form = InstallationReportForm(
request.POST,
instance=existing_report if edit_mode else None,
user_is_installer=user_is_installer,
instance_well=instance.well
)
if form.is_valid():
# Validate photos
photo_validation_passed = False
try:
deleted_photo_ids = []
for key, val in request.POST.items():
if key.startswith('del_photo_') and val == '1':
try:
pid = key.split('_')[-1]
deleted_photo_ids.append(pid)
except Exception:
continue
existing_photos = existing_report.photos.all() if existing_report else None
form.validate_photos(request.FILES, existing_photos, deleted_photo_ids)
photo_validation_passed = True
except ValidationError as e:
form.add_error(None, str(e))
# Re-render form with photo validation error
photo_validation_passed = False
# Always clear approvals/rejections when form is submitted (even if photo validation fails)
# Reset step status and clear approvals/rejections
step_instance.status = 'in_progress'
step_instance.completed_at = None
step_instance.save()
try:
for appr in list(step_instance.approvals.all()):
appr.delete()
except Exception:
pass
try:
for rej in list(step_instance.rejections.all()):
rej.delete()
except Exception:
pass
# Reopen subsequent steps
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':
instance.step_instances.filter(step=subsequent_step).update(
status='in_progress',
completed_at=None
)
try:
for appr in list(subsequent_step_instance.approvals.all()):
appr.delete()
except Exception:
pass
except Exception:
pass
# Reset current step if needed
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
# Only save the report if photo validation passed
if photo_validation_passed:
# Save the form
report = form.save(commit=False)
if not existing_report:
report.assignment = assignment
report.created_by = request.user
report.approved = False # Reset approval status
report.save()
# Handle photo uploads and deletions
if existing_report and edit_mode:
# Delete selected existing photos
for key, val in request.POST.items():
if key.startswith('del_photo_') and val == '1':
try:
pid = int(key.split('_')[-1])
InstallationPhoto.objects.filter(id=pid, report=report).delete()
except Exception:
continue
# Add new photos
for f in request.FILES.getlist('photos'):
InstallationPhoto.objects.create(report=report, image=f)
# Handle item changes (this logic remains the same)
remove_map = {}
add_map = {}
for key in request.POST.keys():
if key.startswith('rem_') and key.endswith('_type'):
try:
item_id = int(key.split('_')[1])
except Exception:
continue
if request.POST.get(key) != 'remove':
continue
qty_val = request.POST.get(f'rem_{item_id}_qty') or '1'
try:
qty = int(qty_val)
except Exception:
qty = 1
remove_map[item_id] = qty
if key.startswith('add_') and key.endswith('_type'):
try:
item_id = int(key.split('_')[1])
except Exception:
continue
if request.POST.get(key) != 'add':
continue
qty_val = request.POST.get(f'add_{item_id}_qty') or '1'
price_val = request.POST.get(f'add_{item_id}_price')
try:
qty = int(qty_val)
except Exception:
qty = 1
# resolve unit price
unit_price = None
if price_val:
try:
unit_price = Decimal(price_val)
except InvalidOperation:
unit_price = None
if unit_price is None:
item_obj = Item.objects.filter(id=item_id).first()
unit_price = item_obj.unit_price if item_obj else None
add_map[item_id] = {'qty': qty, 'price': unit_price}
# Replace item changes with new submission
if existing_report and edit_mode:
report.item_changes.all().delete()
create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
messages.success(request, 'گزارش ثبت شد و در انتظار تایید است.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
else:
# GET request or approval/rejection actions - initialize form for display
form = InstallationReportForm(
instance=existing_report if existing_report else None,
user_is_installer=user_is_installer,
instance_well=instance.well
)
# 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 []
# Align permission check with invoices flow (role id intersection)
try:
req_role_ids = {r.role_id for r in reqs}
user_role_ids = {ur.id for ur in user_roles}
can_approve_reject = len(req_role_ids.intersection(user_role_ids)) > 0
except Exception:
can_approve_reject = False
user_can_approve = can_approve_reject
approvals_list = list(step_instance.approvals.select_related('role', 'approved_by').filter(is_deleted=False))
rejections_list = list(step_instance.rejections.select_related('role', 'rejected_by').filter(is_deleted=False))
approvals_by_role = {a.role_id: a for a in approvals_list}
rejections_by_role = {r.role_id: r for r in rejections_list}
approver_statuses = []
for r in reqs:
appr = approvals_by_role.get(r.role_id)
rejection = rejections_by_role.get(r.role_id)
if appr:
status = 'approved'
reason = appr.reason
elif rejection:
status = 'rejected'
reason = rejection.reason
else:
status = None
reason = ''
approver_statuses.append({
'role': r.role,
'status': status,
'reason': reason,
})
# Determine if current user has already approved/rejected (to disable buttons)
current_user_has_decided = False
try:
user_has_approval = step_instance.approvals.filter(approved_by=request.user, is_deleted=False).exists()
user_has_rejection = step_instance.rejections.filter(rejected_by=request.user, is_deleted=False).exists()
current_user_has_decided = bool(user_has_approval or user_has_rejection)
except Exception:
current_user_has_decided = False
# 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':
# Record this user's approval for their role
StepApproval.objects.create(
step_instance=step_instance,
role=matching_role,
approved_by=request.user,
reason=''
)
# Only mark report approved when ALL required roles have approved
if step_instance.is_fully_approved():
existing_report.approved = True
existing_report.save()
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')
else:
# Not fully approved yet; keep report as not approved
if existing_report.approved:
existing_report.approved = False
existing_report.save(update_fields=['approved'])
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)
# Only create StepRejection for rejections, not StepApproval
StepRejection.objects.create(step_instance=step_instance, role=matching_role, rejected_by=request.user, reason=reason)
existing_report.approved = False
existing_report.save()
# If current step moved ahead of this step, reset it back for correction (align with invoices)
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, 'گزارش رد شد و برای اصلاح به نصاب بازگشت.')
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
# Build prefill maps from existing report changes
removed_ids = set()
removed_qty = {}
added_map = {}
if existing_report:
for ch in existing_report.item_changes.all():
if ch.change_type == 'remove':
removed_ids.add(ch.item_id)
removed_qty[ch.item_id] = ch.quantity
elif ch.change_type == 'add':
added_map[ch.item_id] = {'qty': ch.quantity, 'price': ch.unit_price}
return render(request, 'installations/installation_report_step.html', {
'instance': instance,
'step': step,
'assignment': assignment,
'report': existing_report,
'form': form,
'edit_mode': edit_mode,
'user_is_installer': user_is_installer,
'quote': quote,
'quote_items': quote_items,
'all_items': items,
'manufacturers': manufacturers,
'removed_ids': removed_ids,
'removed_qty': removed_qty,
'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,
'can_approve_reject': can_approve_reject,
'current_user_has_decided': current_user_has_decided,
})