This commit is contained in:
aminhashemi92 2025-09-29 17:38:11 +03:30
parent 810c87e2e0
commit b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions

View file

@ -3,12 +3,15 @@ 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
@ -122,12 +125,9 @@ def installation_report_step(request, instance_id, step_id):
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
# 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')
# 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(
@ -136,6 +136,177 @@ def installation_report_step(request, instance_id, step_id):
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)
@ -148,7 +319,7 @@ def installation_report_step(request, instance_id, step_id):
except Exception:
can_approve_reject = False
user_can_approve = can_approve_reject
approvals_list = list(step_instance.approvals.select_related('role').all())
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 = [
{
@ -159,6 +330,15 @@ def installation_report_step(request, instance_id, step_id):
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')
@ -175,14 +355,16 @@ def installation_report_step(request, instance_id, step_id):
return redirect('processes:step_detail', instance_id=instance.id, step_id=step.id)
if action == 'approve':
existing_report.approved = True
existing_report.save()
# 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()
@ -191,6 +373,11 @@ def installation_report_step(request, instance_id, step_id):
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)
@ -217,160 +404,6 @@ def installation_report_step(request, instance_id, step_id):
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:
visited_date = visited_date.replace('/', '-')
new_serial = (request.POST.get('new_water_meter_serial') or '').strip()
seal_number = (request.POST.get('seal_number') or '').strip()
is_suspicious = True if request.POST.get('is_meter_suspicious') == 'on' else False
utm_x = request.POST.get('utm_x') or None
utm_y = request.POST.get('utm_y') or None
# Normalize UTM to integer meters
if utm_x is not None and utm_x != '':
try:
utm_x = int(Decimal(str(utm_x)))
except InvalidOperation:
utm_x = None
else:
utm_x = None
if utm_y is not None and utm_y != '':
try:
utm_y = int(Decimal(str(utm_y)))
except InvalidOperation:
utm_y = None
else:
utm_y = None
# Build maps from form fields: remove and add
remove_map = {}
add_map = {}
for key in request.POST.keys():
if key.startswith('rem_') and key.endswith('_type'):
# rem_{id}_type = 'remove'
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}
if existing_report and edit_mode:
report = existing_report
report.description = description
report.visited_date = visited_date or None
report.new_water_meter_serial = new_serial or None
report.seal_number = seal_number or None
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():
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
# append new photos
for f in request.FILES.getlist('photos'):
InstallationPhoto.objects.create(report=report, image=f)
# replace item changes with new submission
report.item_changes.all().delete()
create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
else:
report = InstallationReport.objects.create(
assignment=assignment,
description=description,
visited_date=visited_date or None,
new_water_meter_serial=new_serial or None,
seal_number=seal_number or None,
is_meter_suspicious=is_suspicious,
utm_x=utm_x,
utm_y=utm_y,
created_by=request.user,
)
# photos
for f in request.FILES.getlist('photos'):
InstallationPhoto.objects.create(report=report, image=f)
# item changes
create_item_changes_for_report(report, remove_map, add_map, quote_price_map)
# 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 the report was edited, ensure downstream steps reopen like invoices flow
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':
# Reopen the step
instance.step_instances.filter(step=subsequent_step).update(
status='in_progress',
completed_at=None
)
# Clear previous approvals if any
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
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()
@ -389,11 +422,13 @@ def installation_report_step(request, instance_id, step_id):
'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,
@ -403,6 +438,7 @@ def installation_report_step(request, instance_id, step_id):
'approver_statuses': approver_statuses,
'user_can_approve': user_can_approve,
'can_approve_reject': can_approve_reject,
'current_user_has_decided': current_user_has_decided,
})