huge fix
This commit is contained in:
parent
810c87e2e0
commit
b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue