huge fix
This commit is contained in:
parent
810c87e2e0
commit
b5bf3a5dbe
51 changed files with 2397 additions and 326 deletions
|
|
@ -21,10 +21,40 @@ class InstallationItemChangeInline(admin.TabularInline):
|
|||
|
||||
@admin.register(InstallationReport)
|
||||
class InstallationReportAdmin(admin.ModelAdmin):
|
||||
list_display = ('assignment', 'visited_date', 'new_water_meter_serial', 'seal_number', 'is_meter_suspicious', 'approved', 'created')
|
||||
list_filter = ('is_meter_suspicious', 'approved', 'visited_date')
|
||||
search_fields = ('assignment__process_instance__code', 'new_water_meter_serial', 'seal_number')
|
||||
list_display = (
|
||||
'assignment', 'visited_date', 'meter_type', 'meter_size', 'water_meter_manufacturer',
|
||||
'discharge_pipe_diameter', 'usage_type', 'exploitation_license_number',
|
||||
'motor_power', 'pre_calibration_flow_rate', 'post_calibration_flow_rate',
|
||||
'new_water_meter_serial', 'seal_number', 'sim_number',
|
||||
'is_meter_suspicious', 'approved', 'created'
|
||||
)
|
||||
list_filter = ('is_meter_suspicious', 'approved', 'visited_date', 'meter_type', 'usage_type', 'water_meter_manufacturer')
|
||||
search_fields = (
|
||||
'assignment__process_instance__code', 'new_water_meter_serial', 'seal_number', 'exploitation_license_number', 'sim_number'
|
||||
)
|
||||
inlines = [InstallationPhotoInline, InstallationItemChangeInline]
|
||||
fieldsets = (
|
||||
('زمان و تایید', {
|
||||
'fields': ('visited_date', 'approved', 'approved_at')
|
||||
}),
|
||||
('کنتور و سازنده', {
|
||||
'fields': (
|
||||
'meter_type', 'meter_size', 'water_meter_manufacturer', 'new_water_meter_serial', 'seal_number', 'is_meter_suspicious', 'sim_number'
|
||||
)
|
||||
}),
|
||||
('مشخصات هیدرولیکی', {
|
||||
'fields': ('discharge_pipe_diameter', 'pre_calibration_flow_rate', 'post_calibration_flow_rate')
|
||||
}),
|
||||
('کاربری و مجوز', {
|
||||
'fields': ('usage_type', 'exploitation_license_number')
|
||||
}),
|
||||
('توان و محرکه', {
|
||||
'fields': ('driving_force', 'motor_power')
|
||||
}),
|
||||
('توضیحات', {
|
||||
'fields': ('description',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(InstallationPhoto)
|
||||
|
|
|
|||
208
installations/forms.py
Normal file
208
installations/forms.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import InstallationReport
|
||||
from wells.models import WaterMeterManufacturer
|
||||
|
||||
|
||||
class InstallationReportForm(forms.ModelForm):
|
||||
# Additional fields for manufacturer handling
|
||||
new_manufacturer = forms.CharField(
|
||||
max_length=100,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'شرکت سازنده جدید',
|
||||
'style': 'display:none;'
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InstallationReport
|
||||
fields = [
|
||||
'visited_date', 'new_water_meter_serial', 'seal_number',
|
||||
'utm_x', 'utm_y', 'meter_type', 'meter_size',
|
||||
'discharge_pipe_diameter', 'usage_type', 'exploitation_license_number',
|
||||
'motor_power', 'pre_calibration_flow_rate', 'post_calibration_flow_rate',
|
||||
'water_meter_manufacturer', 'sim_number', 'driving_force',
|
||||
'is_meter_suspicious', 'description'
|
||||
]
|
||||
|
||||
widgets = {
|
||||
'visited_date': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'form-control',
|
||||
'required': True
|
||||
}),
|
||||
'new_water_meter_serial': forms.TextInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'seal_number': forms.TextInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'utm_x': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '1'
|
||||
}),
|
||||
'utm_y': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'step': '1'
|
||||
}),
|
||||
'meter_type': forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
}, choices=[
|
||||
('', 'انتخاب کنید'),
|
||||
('smart', 'هوشمند (آبی/برق)'),
|
||||
('volumetric', 'حجمی')
|
||||
]),
|
||||
'meter_size': forms.TextInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'discharge_pipe_diameter': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': '0',
|
||||
'step': '1'
|
||||
}),
|
||||
'usage_type': forms.Select(attrs={
|
||||
'class': 'form-select'
|
||||
}, choices=[
|
||||
('', 'انتخاب کنید'),
|
||||
('domestic', 'شرب و خدمات'),
|
||||
('agriculture', 'کشاورزی'),
|
||||
('industrial', 'صنعتی')
|
||||
]),
|
||||
'exploitation_license_number': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'required': True
|
||||
}),
|
||||
'motor_power': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': '0',
|
||||
'step': '1'
|
||||
}),
|
||||
'pre_calibration_flow_rate': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': '0',
|
||||
'step': '0.01'
|
||||
}),
|
||||
'post_calibration_flow_rate': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': '0',
|
||||
'step': '0.01'
|
||||
}),
|
||||
'water_meter_manufacturer': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
'id': 'id_water_meter_manufacturer'
|
||||
}),
|
||||
'sim_number': forms.TextInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'driving_force': forms.TextInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'is_meter_suspicious': forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input',
|
||||
'id': 'id_is_meter_suspicious'
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 3
|
||||
})
|
||||
}
|
||||
|
||||
labels = {
|
||||
'visited_date': 'تاریخ مراجعه',
|
||||
'new_water_meter_serial': 'سریال کنتور جدید',
|
||||
'seal_number': 'شماره پلمپ',
|
||||
'utm_x': 'UTM X',
|
||||
'utm_y': 'UTM Y',
|
||||
'meter_type': 'نوع کنتور',
|
||||
'meter_size': 'سایز کنتور',
|
||||
'discharge_pipe_diameter': 'قطر لوله آبده (اینچ)',
|
||||
'usage_type': 'نوع مصرف',
|
||||
'exploitation_license_number': 'شماره پروانه بهرهبرداری',
|
||||
'motor_power': 'قدرت موتور (کیلووات ساعت)',
|
||||
'pre_calibration_flow_rate': 'دبی قبل از کالیبراسیون (لیتر بر ثانیه)',
|
||||
'post_calibration_flow_rate': 'دبی بعد از کالیبراسیون (لیتر بر ثانیه)',
|
||||
'water_meter_manufacturer': 'شرکت سازنده کنتور',
|
||||
'sim_number': 'شماره سیمکارت',
|
||||
'driving_force': 'نیرو محرکه چاه',
|
||||
'is_meter_suspicious': 'کنتور مشکوک است',
|
||||
'description': 'توضیحات'
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user_is_installer = kwargs.pop('user_is_installer', False)
|
||||
self.instance_well = kwargs.pop('instance_well', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set manufacturer choices
|
||||
manufacturers = WaterMeterManufacturer.objects.filter(is_deleted=False)
|
||||
manufacturer_choices = [('', 'انتخاب شرکت سازنده')]
|
||||
manufacturer_choices.extend([(m.id, m.name) for m in manufacturers])
|
||||
self.fields['water_meter_manufacturer'].choices = manufacturer_choices
|
||||
|
||||
# Pre-fill UTM from well if available and no existing report data
|
||||
if self.instance_well and not self.instance.pk:
|
||||
if self.instance_well.utm_x:
|
||||
self.initial['utm_x'] = self.instance_well.utm_x
|
||||
if self.instance_well.utm_y:
|
||||
self.initial['utm_y'] = self.instance_well.utm_y
|
||||
|
||||
# Disable fields for non-installers
|
||||
if not self.user_is_installer:
|
||||
for field_name, field in self.fields.items():
|
||||
if field_name != 'new_manufacturer': # Keep this always disabled via CSS
|
||||
field.widget.attrs['readonly'] = True
|
||||
if isinstance(field.widget, (forms.Select, forms.CheckboxInput)):
|
||||
field.widget.attrs['disabled'] = True
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Handle new manufacturer creation
|
||||
new_manufacturer = cleaned_data.get('new_manufacturer')
|
||||
water_meter_manufacturer = cleaned_data.get('water_meter_manufacturer')
|
||||
|
||||
if new_manufacturer and not water_meter_manufacturer:
|
||||
# Create new manufacturer
|
||||
manufacturer, created = WaterMeterManufacturer.objects.get_or_create(
|
||||
name=new_manufacturer,
|
||||
defaults={'is_deleted': False}
|
||||
)
|
||||
cleaned_data['water_meter_manufacturer'] = manufacturer
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_visited_date(self):
|
||||
visited_date = self.cleaned_data.get('visited_date')
|
||||
if not visited_date:
|
||||
raise ValidationError('تاریخ مراجعه الزامی است.')
|
||||
return visited_date
|
||||
|
||||
def clean_exploitation_license_number(self):
|
||||
license_number = self.cleaned_data.get('exploitation_license_number')
|
||||
if not license_number or not license_number.strip():
|
||||
raise ValidationError('شماره پروانه بهرهبرداری الزامی است.')
|
||||
return license_number.strip()
|
||||
|
||||
def validate_photos(self, request_files, existing_photos, deleted_photo_ids):
|
||||
"""
|
||||
Validate that at least one photo is present (either existing or newly uploaded)
|
||||
This method should be called from the view after form.is_valid()
|
||||
"""
|
||||
# Count existing photos that are not deleted
|
||||
kept_existing = 0
|
||||
if existing_photos:
|
||||
for photo in existing_photos:
|
||||
if str(photo.id) not in deleted_photo_ids:
|
||||
kept_existing += 1
|
||||
|
||||
# Count new photos
|
||||
new_photos = len(request_files.getlist('photos')) if request_files else 0
|
||||
|
||||
total_photos = kept_existing + new_photos
|
||||
|
||||
if total_photos <= 0:
|
||||
raise ValidationError('بارگذاری حداقل یک عکس الزامی است.')
|
||||
|
||||
return True
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-21 07:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('installations', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='installationreport',
|
||||
name='utm_x',
|
||||
field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='UTM X'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='installationreport',
|
||||
name='utm_y',
|
||||
field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='UTM Y'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-24 11:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('installations', '0002_alter_installationreport_utm_x_and_more'),
|
||||
('wells', '0004_remove_historicalwell_discharge_pipe_diameter_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='discharge_pipe_diameter',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قطر لوله آبده (اینچ)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='driving_force',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='نیرو محرکه چاه'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='exploitation_license_number',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='meter_size',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='سایز کنتور'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='meter_type',
|
||||
field=models.CharField(blank=True, choices=[('smart', 'هوشمند (آبی/برق)'), ('volumetric', 'حجمی')], max_length=20, null=True, verbose_name='نوع کنتور'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='motor_power',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت)قدرت موتور'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='post_calibration_flow_rate',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='pre_calibration_flow_rate',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='sim_number',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='شماره سیمکارت'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='usage_type',
|
||||
field=models.CharField(blank=True, choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='installationreport',
|
||||
name='water_meter_manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wells.watermetermanufacturer', verbose_name='شرکت سازنده کنتور آب'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-27 06:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('installations', '0003_installationreport_discharge_pipe_diameter_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='installationreport',
|
||||
name='motor_power',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='(کیلووات ساعت) قدرت موتور'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-29 10:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('installations', '0004_alter_installationreport_motor_power'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='installationreport',
|
||||
name='usage_type',
|
||||
field=models.CharField(choices=[('domestic', 'شرب و خدمات'), ('agriculture', 'کشاورزی'), ('industrial', 'صنعتی')], max_length=20, null=True, verbose_name='نوع مصرف'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.2.4 on 2025-09-29 10:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('installations', '0005_alter_installationreport_usage_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='installationreport',
|
||||
name='exploitation_license_number',
|
||||
field=models.CharField(default=1, max_length=50, verbose_name='شماره پروانه بهره\u200cبرداری چاه'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
@ -42,6 +42,26 @@ 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='کنتور مشکوک است؟')
|
||||
METER_TYPE_CHOICES = [
|
||||
('smart', 'هوشمند (آبی/برق)'),
|
||||
('volumetric', 'حجمی'),
|
||||
]
|
||||
meter_type = models.CharField(max_length=20, choices=METER_TYPE_CHOICES, null=True, blank=True, verbose_name='نوع کنتور')
|
||||
meter_size = models.CharField(max_length=50, null=True, blank=True, verbose_name='سایز کنتور')
|
||||
discharge_pipe_diameter = models.PositiveIntegerField(null=True, blank=True, verbose_name='قطر لوله آبده (اینچ)')
|
||||
USAGE_TYPE_CHOICES = [
|
||||
('domestic', 'شرب و خدمات'),
|
||||
('agriculture', 'کشاورزی'),
|
||||
('industrial', 'صنعتی'),
|
||||
]
|
||||
usage_type = models.CharField(max_length=20, choices=USAGE_TYPE_CHOICES, null=True, verbose_name='نوع مصرف')
|
||||
exploitation_license_number = models.CharField(max_length=50, verbose_name='شماره پروانه بهرهبرداری چاه')
|
||||
motor_power = models.PositiveIntegerField(null=True, blank=True, verbose_name='(کیلووات ساعت) قدرت موتور')
|
||||
pre_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی قبل از کالیبراسیون')
|
||||
post_calibration_flow_rate = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='(لیتر بر ثانیه)دبی بعد از کالیبراسیون')
|
||||
water_meter_manufacturer = models.ForeignKey('wells.WaterMeterManufacturer', on_delete=models.SET_NULL, null=True, blank=True, verbose_name='شرکت سازنده کنتور آب')
|
||||
sim_number = models.CharField(max_length=20, null=True, blank=True, verbose_name='شماره سیمکارت')
|
||||
driving_force = models.CharField(max_length=50, null=True, blank=True, verbose_name='نیرو محرکه چاه')
|
||||
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='توضیحات')
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
<div class="bs-stepper-content">
|
||||
{% if report and not edit_mode %}
|
||||
<div class="mb-3 text-end">
|
||||
{% if user_is_installer %}
|
||||
{% if user_is_installer and not report.approved %}
|
||||
<a href="?edit=1" class="btn btn-primary">
|
||||
<i class="bx bx-edit bx-sm me-2"></i>
|
||||
ویرایش گزارش نصب
|
||||
|
|
@ -86,11 +86,23 @@
|
|||
<p class="text-nowrap mb-2"><i class="bx bx-calendar-event bx-sm me-2"></i>تاریخ مراجعه: {{ report.visited_date|to_jalali|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-purchase-tag bx-sm me-2"></i>سریال جدید: {{ report.new_water_meter_serial|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-lock-alt bx-sm me-2"></i>شماره پلمپ: {{ report.seal_number|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-chip bx-sm me-2"></i>نوع کنتور: {{ report.get_meter_type_display|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-ruler bx-sm me-2"></i>سایز کنتور: {{ report.meter_size|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-tachometer bx-sm me-2"></i>قطر لوله آبده (اینچ): {{ report.discharge_pipe_diameter|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-building bx-sm me-2"></i>سازنده کنتور: {{ report.water_meter_manufacturer|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-sim-card bx-sm me-2"></i>شماره سیمکارت: {{ report.sim_number|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-cog bx-sm me-2"></i>نیرو محرکه چاه: {{ report.driving_force|default:'-' }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-help-circle bx-sm me-2"></i>کنتور مشکوک: {{ report.is_meter_suspicious|yesno:'بله,خیر' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-map bx-sm me-2"></i>UTM X: {{ report.utm_x|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-map-pin bx-sm me-2"></i>UTM Y: {{ report.utm_y|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-category bx-sm me-2"></i>نوع مصرف: {{ report.get_usage_type_display|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-id-card bx-sm me-2"></i>شماره پروانه بهرهبرداری: {{ report.exploitation_license_number|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-bolt-circle bx-sm me-2"></i>(کیلووات ساعت)قدرت موتور: {{ report.motor_power|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی قبل کالیبراسیون: {{ report.pre_calibration_flow_rate|default:'-' }}</p>
|
||||
<p class="text-nowrap mb-2"><i class="bx bx-water bx-sm me-2"></i>(لیتر/ثانیه) دبی بعد کالیبراسیون: {{ report.post_calibration_flow_rate|default:'-' }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if report.description %}
|
||||
|
|
@ -155,11 +167,18 @@
|
|||
<div class="card border mt-2">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">وضعیت تاییدها</h6>
|
||||
{% if user_can_approve %}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal">تایید</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
|
||||
</div>
|
||||
{% if can_approve_reject %}
|
||||
{% if current_user_has_decided %}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success btn-sm" disabled>تایید</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" disabled>رد</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#approveModal">تایید</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#rejectModal">رد</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body py-3">
|
||||
|
|
@ -213,41 +232,149 @@
|
|||
{% if user_is_installer %}
|
||||
<!-- Installation Report Form -->
|
||||
<form method="post" enctype="multipart/form-data" id="installation-report-form">
|
||||
{% csrf_token %}
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<div class="">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">تاریخ مراجعه</label>
|
||||
<input type="text" id="id_visited_date_display" class="form-control" placeholder="انتخاب تاریخ" {% if not user_is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% endif %}">
|
||||
<input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% endif %}">
|
||||
{{ form.visited_date.label_tag }}
|
||||
<!-- Custom date picker handling -->
|
||||
<input type="text" id="id_visited_date_display" class="form-control{% if form.visited_date.errors %} is-invalid{% endif %}" placeholder="انتخاب تاریخ" {% if not user_is_installer %}disabled{% endif %} readonly required value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y/m/d' }}{% elif form.visited_date.value %}{{ form.visited_date.value|date:'Y/m/d' }}{% endif %}">
|
||||
<input type="hidden" id="id_visited_date" name="visited_date" value="{% if report and edit_mode and report.visited_date %}{{ report.visited_date|date:'Y-m-d' }}{% elif form.visited_date.value %}{{ form.visited_date.value }}{% endif %}">
|
||||
{% if form.visited_date.errors %}
|
||||
<div class="invalid-feedback">{{ form.visited_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">سریال کنتور جدید</label>
|
||||
<input type="text" class="form-control" name="new_water_meter_serial" value="{% if report and edit_mode %}{{ report.new_water_meter_serial|default_if_none:'' }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
|
||||
{{ form.new_water_meter_serial.label_tag }}
|
||||
{{ form.new_water_meter_serial }}
|
||||
{% if form.new_water_meter_serial.errors %}
|
||||
<div class="invalid-feedback">{{ form.new_water_meter_serial.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">شماره پلمپ</label>
|
||||
<input type="text" class="form-control" name="seal_number" value="{% if report and edit_mode %}{{ report.seal_number|default_if_none:'' }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
|
||||
{{ form.seal_number.label_tag }}
|
||||
{{ form.seal_number }}
|
||||
{% if form.seal_number.errors %}
|
||||
<div class="invalid-feedback">{{ form.seal_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.utm_x.label_tag }}
|
||||
{{ form.utm_x }}
|
||||
{% if form.utm_x.errors %}
|
||||
<div class="invalid-feedback">{{ form.utm_x.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.utm_y.label_tag }}
|
||||
{{ form.utm_y }}
|
||||
{% if form.utm_y.errors %}
|
||||
<div class="invalid-feedback">{{ form.utm_y.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.meter_type.label_tag }}
|
||||
{{ form.meter_type }}
|
||||
{% if form.meter_type.errors %}
|
||||
<div class="invalid-feedback">{{ form.meter_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.meter_size.label_tag }}
|
||||
{{ form.meter_size }}
|
||||
{% if form.meter_size.errors %}
|
||||
<div class="invalid-feedback">{{ form.meter_size.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.discharge_pipe_diameter.label_tag }}
|
||||
{{ form.discharge_pipe_diameter }}
|
||||
{% if form.discharge_pipe_diameter.errors %}
|
||||
<div class="invalid-feedback">{{ form.discharge_pipe_diameter.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.usage_type.label_tag }}
|
||||
{{ form.usage_type }}
|
||||
{% if form.usage_type.errors %}
|
||||
<div class="invalid-feedback">{{ form.usage_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.exploitation_license_number.label_tag }}
|
||||
{{ form.exploitation_license_number }}
|
||||
{% if form.exploitation_license_number.errors %}
|
||||
<div class="invalid-feedback">{{ form.exploitation_license_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.motor_power.label_tag }}
|
||||
{{ form.motor_power }}
|
||||
{% if form.motor_power.errors %}
|
||||
<div class="invalid-feedback">{{ form.motor_power.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.pre_calibration_flow_rate.label_tag }}
|
||||
{{ form.pre_calibration_flow_rate }}
|
||||
{% if form.pre_calibration_flow_rate.errors %}
|
||||
<div class="invalid-feedback">{{ form.pre_calibration_flow_rate.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.post_calibration_flow_rate.label_tag }}
|
||||
{{ form.post_calibration_flow_rate }}
|
||||
{% if form.post_calibration_flow_rate.errors %}
|
||||
<div class="invalid-feedback">{{ form.post_calibration_flow_rate.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.water_meter_manufacturer.label_tag }}
|
||||
<div class="input-group">
|
||||
{{ form.water_meter_manufacturer }}
|
||||
{{ form.new_manufacturer }}
|
||||
{% if user_is_installer %}
|
||||
<button class="btn btn-outline-primary" type="button" id="btnToggleManufacturer"><i class="bx bx-plus"></i></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.water_meter_manufacturer.errors %}
|
||||
<div class="invalid-feedback">{{ form.water_meter_manufacturer.errors.0 }}</div>
|
||||
{% endif %}
|
||||
{% if form.new_manufacturer.errors %}
|
||||
<div class="invalid-feedback">{{ form.new_manufacturer.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.sim_number.label_tag }}
|
||||
{{ form.sim_number }}
|
||||
{% if form.sim_number.errors %}
|
||||
<div class="invalid-feedback">{{ form.sim_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ form.driving_force.label_tag }}
|
||||
{{ form.driving_force }}
|
||||
{% if form.driving_force.errors %}
|
||||
<div class="invalid-feedback">{{ form.driving_force.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_meter_suspicious" id="id_is_meter_suspicious" {% if not user_is_installer %}disabled{% endif %} {% if report and edit_mode and report.is_meter_suspicious %}checked{% endif %}>
|
||||
<label class="form-check-label" for="id_is_meter_suspicious">کنتور مشکوک است</label>
|
||||
{{ form.is_meter_suspicious }}
|
||||
{{ form.is_meter_suspicious.label_tag }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">UTM X</label>
|
||||
<input type="number" step="1" class="form-control" name="utm_x" value="{% if report and edit_mode and report.utm_x %}{{ report.utm_x }}{% elif instance.well.utm_x %}{{ instance.well.utm_x }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">UTM Y</label>
|
||||
<input type="number" step="1" class="form-control" name="utm_y" value="{% if report and edit_mode and report.utm_y %}{{ report.utm_y }}{% elif instance.well.utm_y %}{{ instance.well.utm_y }}{% endif %}" {% if not user_is_installer %}readonly{% endif %}>
|
||||
{% if form.is_meter_suspicious.errors %}
|
||||
<div class="invalid-feedback">{{ form.is_meter_suspicious.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label class="form-label">توضیحات (اختیاری)</label>
|
||||
<textarea class="form-control" rows="3" name="description" {% if not user_is_installer %}readonly{% endif %}>{% if report and edit_mode %}{{ report.description|default_if_none:'' }}{% endif %}</textarea>
|
||||
{{ form.description.label_tag }}
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback">{{ form.description.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
|
@ -284,7 +411,7 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="col-12 mb-4 d-none">
|
||||
<h6 class="mb-2">اقلام انتخابشده قبلی <small class="text-muted">(برای حذف در نصب تیک بزنید)</small></h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
|
|
@ -321,7 +448,6 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="col-12">
|
||||
<h6 class="mb-2">افزودن اقلام جدید</h6>
|
||||
<div class="table-responsive">
|
||||
|
|
@ -513,6 +639,20 @@
|
|||
display.scrollIntoView({behavior:'smooth', block:'center'});
|
||||
return false;
|
||||
}
|
||||
// Require at least one photo: either existing (not marked for deletion) or newly added
|
||||
try {
|
||||
var keptExisting = 0;
|
||||
document.querySelectorAll('input[id^="del-photo-"]').forEach(function(inp){
|
||||
if (String(inp.value) !== '1') keptExisting += 1;
|
||||
});
|
||||
var newFiles = document.querySelectorAll('#photoInputs input[type="file"]').length;
|
||||
if ((keptExisting + newFiles) <= 0) {
|
||||
ev.preventDefault(); ev.stopPropagation();
|
||||
showToast('بارگذاری حداقل یک عکس الزامی است', 'danger');
|
||||
(document.getElementById('btnAddPhoto') || form).scrollIntoView({behavior:'smooth', block:'center'});
|
||||
return false;
|
||||
}
|
||||
} catch(_) {}
|
||||
try { sessionStorage.setItem('install_report_saved', '1'); } catch(_) {}
|
||||
}, false);
|
||||
// on load, if saved flag exists, show toast
|
||||
|
|
@ -568,6 +708,36 @@
|
|||
if (btnAddPhoto) btnAddPhoto.addEventListener('click', createPhotoInput);
|
||||
})();
|
||||
|
||||
// Toggle manufacturer select/input (like request_list)
|
||||
(function(){
|
||||
const $select = $('#id_water_meter_manufacturer');
|
||||
const $input = $('#id_new_manufacturer');
|
||||
const $btn = $('#btnToggleManufacturer');
|
||||
if (!$select.length || !$btn.length) return;
|
||||
$btn.on('click', function(){
|
||||
if ($select.is(':visible')) {
|
||||
$select.hide();
|
||||
$input.show().focus();
|
||||
$btn.html('<i class="bx bx-check"></i>');
|
||||
} else {
|
||||
// When switching back, if input has value, append it as selected option
|
||||
const val = ($input.val() || '').trim();
|
||||
if (val) {
|
||||
// Add a temporary option with value prefixed 'new:' to be handled server-side
|
||||
const exists = $select.find('option').filter(function(){ return $(this).text().trim() === val; }).length > 0;
|
||||
if (!exists) {
|
||||
const opt = $('<option></option>').val('').text(val);
|
||||
$select.append(opt);
|
||||
}
|
||||
$select.val('');
|
||||
}
|
||||
$input.hide();
|
||||
$select.show();
|
||||
$btn.html('<i class="bx bx-plus"></i>');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Mark delete for existing photos
|
||||
function markDeletePhoto(id){
|
||||
const hidden = document.getElementById('del-photo-' + id);
|
||||
|
|
|
|||
|
|
@ -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