444 lines
20 KiB
Python
444 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))
|
|
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
|
|
]
|
|
|
|
# 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.update_or_create(
|
|
step_instance=step_instance,
|
|
role=matching_role,
|
|
defaults={'approved_by': request.user, 'decision': 'approved', '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)
|
|
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()
|
|
# 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,
|
|
})
|
|
|
|
|