تاریخ مراجعه: {{ 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 %}
+
-
+ {% 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