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 accounts.models import Profile from common.consts import UserRoles from processes.models import ProcessInstance, StepInstance from invoices.models import Item, Quote, QuoteItem from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange from decimal import Decimal, InvalidOperation @login_required def installation_assign_step(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=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).select_related('user').all() assignment, _ = InstallationAssignment.objects.get_or_create(process_instance=instance) if request.method == 'POST': 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') return render(request, 'installations/installation_assign_step.html', { 'instance': instance, 'step': step, 'assignment': assignment, 'installers': installers, 'previous_step': previous_step, 'next_step': next_step, }) @login_required def installation_report_step(request, instance_id, step_id): instance = get_object_or_404(ProcessInstance, id=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() edit_mode = True if request.GET.get('edit') == '1' else False print("edit_mode", edit_mode) # 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.all().order_by('name') if request.method == 'POST': 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 # 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.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() 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, ) 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, ) 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 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, ) 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, ) # 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') # 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, 'edit_mode': edit_mode, 'quote': quote, 'quote_items': quote_items, 'all_items': items, 'removed_ids': removed_ids, 'removed_qty': removed_qty, 'added_map': added_map, 'previous_step': previous_step, 'next_step': next_step, })