diff --git a/db.sqlite3 b/db.sqlite3 index a04ea6c..2f70da4 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/installations/models.py b/installations/models.py index 62f7ecc..98fc452 100644 --- a/installations/models.py +++ b/installations/models.py @@ -42,8 +42,8 @@ class InstallationReport(BaseModel): new_water_meter_serial = models.CharField(max_length=50, null=True, blank=True, verbose_name='سریال کنتور جدید') seal_number = models.CharField(max_length=50, null=True, blank=True, verbose_name='شماره پلمپ') is_meter_suspicious = models.BooleanField(default=False, verbose_name='کنتور مشکوک است؟') - utm_x = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True, verbose_name='UTM X') - utm_y = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True, verbose_name='UTM Y') + utm_x = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, verbose_name='UTM X') + utm_y = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, verbose_name='UTM Y') description = models.TextField(blank=True, verbose_name='توضیحات') created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='ایجادکننده') approved = models.BooleanField(default=False, verbose_name='تایید شده') diff --git a/installations/templates/installations/installation_assign_step.html b/installations/templates/installations/installation_assign_step.html index 2bdbe4d..f78b3d8 100644 --- a/installations/templates/installations/installation_assign_step.html +++ b/installations/templates/installations/installation_assign_step.html @@ -22,7 +22,12 @@ {% endblock %} {% block content %} + {% include '_toasts.html' %} + + +{% instance_info_modal instance %} +
@@ -30,11 +35,13 @@

{{ step.name }}: {{ instance.process.name }}

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
- بازگشت + + + بازگشت +
@@ -64,17 +71,17 @@
{% if assignment.assigned_by or assignment.installer %} -
+
{% if assignment.assigned_by %}
-
تعیین‌کننده نصاب
+
تعیین‌کننده نصاب
{{ assignment.assigned_by.get_full_name|default:assignment.assigned_by.username }} ({{ assignment.assigned_by.username }})
{% endif %} {% if assignment.updated %}
-
تاریخ ثبت/ویرایش
+
تاریخ ثبت/ویرایش
{{ assignment.updated|to_jalali }}
{% endif %} @@ -83,14 +90,22 @@ {% endif %}
{% if previous_step %} - قبلی + + + قبلی + {% else %} {% endif %} {% if is_manager %} - + {% else %} - بعدی + + بعدی + + {% endif %}
diff --git a/installations/templates/installations/installation_report_step.html b/installations/templates/installations/installation_report_step.html index 6044f53..946c7b4 100644 --- a/installations/templates/installations/installation_report_step.html +++ b/installations/templates/installations/installation_report_step.html @@ -35,7 +35,12 @@ {% endblock %} {% block content %} + {% include '_toasts.html' %} + + +{% instance_info_modal instance %} +
@@ -43,11 +48,13 @@

{{ step.name }}: {{ instance.process.name }}

- اشتراک آب: {{ instance.well.water_subscription_number|default:"-" }} - | نماینده: {{ instance.representative.profile.national_code|default:"-" }} + {% instance_info instance %}
- بازگشت + + + بازگشت +
@@ -55,18 +62,15 @@
{% if report and not edit_mode %} -
-
-
- {% if request.user|is_installer %} - ویرایش گزارش نصب - {% else %} - - {% endif %} -
-
-
- {% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %} +
+ {% if user_is_installer %} + + + ویرایش گزارش نصب + + {% endif %} +
+ {% if step_instance and step_instance.status == 'rejected' and step_instance.get_latest_rejection %} {% endif %} +
+

تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}

@@ -151,7 +157,7 @@
وضعیت تاییدها
{% if user_can_approve %}
- +
{% endif %} @@ -184,18 +190,28 @@
{% if previous_step %} - قبلی + + + قبلی + {% else %} {% endif %} {% if next_step %} - بعدی + + بعدی + + {% endif %}
{% else %} - {% if not request.user|is_installer %} -
شما مجوز ثبت/ویرایش گزارش نصب را ندارید. اطلاعات به صورت فقط خواندنی نمایش داده می‌شود.
+ + {% if not user_is_installer %} +
شما مجوز ثبت/ویرایش گزارش نصب را ندارید.
{% endif %} + + {% if user_is_installer %} +
{% csrf_token %}
@@ -203,40 +219,40 @@
- +
- +
- +
- +
- +
- +
- +
- {% if request.user|is_installer %} + {% if user_is_installer %} {% endif %}
@@ -246,7 +262,7 @@
photo - {% if request.user|is_installer %} + {% if user_is_installer %} {% endif %} @@ -347,24 +363,28 @@
- +
- + {% endif %}
{% if previous_step %} - قبلی + + + قبلی + {% else %} {% endif %}
- {% if request.user|is_installer %} - - {% else %} - + {% if user_is_installer %} + {% endif %} {% if next_step %} - بعدی + + بعدی + + {% endif %}
@@ -499,7 +519,6 @@ try { if (sessionStorage.getItem('install_report_saved') === '1') { sessionStorage.removeItem('install_report_saved'); - showToast('گزارش نصب با موفقیت ثبت شد', 'success'); } } catch(_) {} })(); diff --git a/installations/views.py b/installations/views.py index 8c3dc7e..ac27db9 100644 --- a/installations/views.py +++ b/installations/views.py @@ -19,7 +19,7 @@ def installation_assign_step(request, instance_id, step_id): 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() + 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 @@ -72,17 +72,56 @@ def installation_assign_step(request, instance_id, step_id): }) +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_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() - # Only installers can enter edit mode - user_is_installer = hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.INSTALLER) + + # 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 + # current quote items baseline quote = Quote.objects.filter(process_instance=instance).first() quote_items = list(quote.items.select_related('item').all()) if quote else [] @@ -100,7 +139,14 @@ def installation_report_step(request, instance_id, step_id): 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 [] - user_can_approve = any(r.role in user_roles for r in reqs) + # 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').all()) approvals_by_role = {a.role_id: a for a in approvals_list} approver_statuses = [ @@ -160,6 +206,13 @@ def installation_report_step(request, instance_id, step_id): 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) @@ -177,6 +230,21 @@ def installation_report_step(request, instance_id, step_id): 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 = {} @@ -221,8 +289,6 @@ def installation_report_step(request, instance_id, step_id): 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 @@ -247,29 +313,7 @@ def installation_report_step(request, instance_id, step_id): 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, - ) + create_item_changes_for_report(report, remove_map, add_map, quote_price_map) else: report = InstallationReport.objects.create( assignment=assignment, @@ -286,29 +330,7 @@ def installation_report_step(request, instance_id, step_id): 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, - ) + 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' @@ -319,6 +341,33 @@ def installation_report_step(request, instance_id, step_id): 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) @@ -340,6 +389,7 @@ def installation_report_step(request, instance_id, step_id): 'assignment': assignment, 'report': existing_report, 'edit_mode': edit_mode, + 'user_is_installer': user_is_installer, 'quote': quote, 'quote_items': quote_items, 'all_items': items, @@ -351,6 +401,7 @@ def installation_report_step(request, instance_id, step_id): 'step_instance': step_instance, 'approver_statuses': approver_statuses, 'user_can_approve': user_can_approve, + 'can_approve_reject': can_approve_reject, }) diff --git a/invoices/views.py b/invoices/views.py index b271a29..ea99eb7 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -408,7 +408,7 @@ def quote_payment_step(request, instance_id, step_id): defaults={'approved_by': request.user, 'decision': 'rejected', 'reason': reason} ) StepRejection.objects.create(step_instance=step_instance, rejected_by=request.user, reason=reason) - # If current step is ahead of this step, reset it back to this step + # 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 diff --git a/locations/models.py b/locations/models.py index d3de371..bca8971 100644 --- a/locations/models.py +++ b/locations/models.py @@ -11,7 +11,7 @@ class City(NameSlugModel): return self.name class County(NameSlugModel): - city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="شهرستان") + city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="استان") class Meta: verbose_name = "شهرستان" diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index dfcf4bc..feb1294 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -479,7 +479,7 @@ $('#requests-table').DataTable({ pageLength: 10, lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]], - order: [[0, 'desc']], + order: [], responsive: true, }); let currentWellId = null; diff --git a/wells/forms.py b/wells/forms.py index 1649d01..5f39178 100644 --- a/wells/forms.py +++ b/wells/forms.py @@ -82,12 +82,10 @@ class WellForm(forms.ModelForm): 'utm_x': forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'X UTM', - 'step': '0.000001' }), 'utm_y': forms.NumberInput(attrs={ 'class': 'form-control', 'placeholder': 'Y UTM', - 'step': '0.000001' }), 'utm_zone': forms.NumberInput(attrs={ 'class': 'form-control', diff --git a/wells/models.py b/wells/models.py index c0663f9..89e9aad 100644 --- a/wells/models.py +++ b/wells/models.py @@ -78,14 +78,14 @@ class Well(SluggedModel): utm_x = models.DecimalField( max_digits=10, - decimal_places=6, + decimal_places=0, verbose_name="X UTM", null=True, blank=True ) utm_y = models.DecimalField( max_digits=10, - decimal_places=6, + decimal_places=0, verbose_name="Y UTM", null=True, blank=True