diff --git a/.DS_Store b/.DS_Store index 946362f6..6d916a35 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/db.sqlite3 b/db.sqlite3 index f686d3df..1a328e1e 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc b/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc index 386b2a09..9e55dd05 100644 Binary files a/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc and b/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc differ diff --git a/emr/templates/emr/problems/problem_list.html b/emr/templates/emr/problems/problem_list.html index 7900b8da..90eaf95c 100644 --- a/emr/templates/emr/problems/problem_list.html +++ b/emr/templates/emr/problems/problem_list.html @@ -67,10 +67,10 @@ -
| Patient | Problem | diff --git a/facility_management/__pycache__/forms.cpython-312.pyc b/facility_management/__pycache__/forms.cpython-312.pyc new file mode 100644 index 00000000..9d342702 Binary files /dev/null and b/facility_management/__pycache__/forms.cpython-312.pyc differ diff --git a/facility_management/__pycache__/models.cpython-312.pyc b/facility_management/__pycache__/models.cpython-312.pyc index 0a9e5c1f..098825ea 100644 Binary files a/facility_management/__pycache__/models.cpython-312.pyc and b/facility_management/__pycache__/models.cpython-312.pyc differ diff --git a/facility_management/__pycache__/urls.cpython-312.pyc b/facility_management/__pycache__/urls.cpython-312.pyc new file mode 100644 index 00000000..34e9bbbb Binary files /dev/null and b/facility_management/__pycache__/urls.cpython-312.pyc differ diff --git a/facility_management/__pycache__/views.cpython-312.pyc b/facility_management/__pycache__/views.cpython-312.pyc new file mode 100644 index 00000000..d48f1407 Binary files /dev/null and b/facility_management/__pycache__/views.cpython-312.pyc differ diff --git a/facility_management/forms.py b/facility_management/forms.py index d2b12b0e..34b77050 100644 --- a/facility_management/forms.py +++ b/facility_management/forms.py @@ -69,10 +69,8 @@ class RoomForm(forms.ModelForm): class Meta: model = Room fields = [ - 'floor', 'room_number', 'name', 'room_type', 'area_sqm', - 'capacity', 'occupancy_status', 'is_accessible', 'current_tenant', - 'lease_start_date', 'lease_end_date', 'monthly_rent', - 'has_hvac', 'has_electrical', 'has_plumbing', 'has_internet', 'notes' + 'floor', 'room_number', 'name', 'area_sqm', + 'capacity', 'occupancy_status', 'is_accessible', 'notes' ] widgets = { 'floor': forms.Select(attrs={'class': 'form-select'}), @@ -81,26 +79,13 @@ class RoomForm(forms.ModelForm): 'room_type': forms.Select(attrs={'class': 'form-select'}), 'area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'capacity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), - 'occupancy_status': forms.Select(attrs={'class': 'form-select'}), 'is_accessible': forms.CheckboxInput(attrs={'class': 'form-check-input'}), - 'current_tenant': forms.TextInput(attrs={'class': 'form-control'}), - 'lease_start_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), - 'lease_end_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), - 'monthly_rent': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), - 'has_hvac': forms.CheckboxInput(attrs={'class': 'form-check-input'}), - 'has_electrical': forms.CheckboxInput(attrs={'class': 'form-check-input'}), - 'has_plumbing': forms.CheckboxInput(attrs={'class': 'form-check-input'}), - 'has_internet': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), } def clean(self): cleaned_data = super().clean() - lease_start = cleaned_data.get('lease_start_date') - lease_end = cleaned_data.get('lease_end_date') - if lease_start and lease_end and lease_start >= lease_end: - raise ValidationError('Lease end date must be after lease start date.') return cleaned_data @@ -132,7 +117,7 @@ class AssetForm(forms.ModelForm): class Meta: model = Asset fields = [ - 'asset_id', 'name', 'category', 'building', 'floor', 'room', + 'name', 'category', 'building', 'floor', 'room', 'location_description', 'manufacturer', 'model', 'serial_number', 'purchase_date', 'purchase_cost', 'current_value', 'depreciation_rate', 'warranty_start_date', 'warranty_end_date', 'service_provider', @@ -140,7 +125,6 @@ class AssetForm(forms.ModelForm): 'last_inspection_date', 'next_maintenance_date', 'assigned_to', 'notes' ] widgets = { - 'asset_id': forms.TextInput(attrs={'class': 'form-control'}), 'name': forms.TextInput(attrs={'class': 'form-control'}), 'category': forms.Select(attrs={'class': 'form-select'}), 'building': forms.Select(attrs={'class': 'form-select'}), @@ -271,7 +255,7 @@ class MaintenanceScheduleForm(forms.ModelForm): model = MaintenanceSchedule fields = [ 'name', 'description', 'maintenance_type', 'asset', 'building', - 'room', 'frequency', 'frequency_interval', 'start_date', + 'room', 'frequency_interval', 'start_date', 'end_date', 'assigned_to', 'estimated_duration_hours', 'is_active' ] widgets = { @@ -322,8 +306,7 @@ class VendorForm(forms.ModelForm): model = Vendor fields = [ 'name', 'vendor_type', 'contact_person', 'email', 'phone', - 'address', 'license_number', 'insurance_policy', - 'insurance_expiry', 'rating', 'is_active' + 'address', 'crn', 'vrn', 'rating', 'is_active' ] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), @@ -332,9 +315,8 @@ class VendorForm(forms.ModelForm): 'email': forms.EmailInput(attrs={'class': 'form-control'}), 'phone': forms.TextInput(attrs={'class': 'form-control'}), 'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), - 'license_number': forms.TextInput(attrs={'class': 'form-control'}), - 'insurance_policy': forms.TextInput(attrs={'class': 'form-control'}), - 'insurance_expiry': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), + 'crn': forms.TextInput(attrs={'class': 'form-control'}), + 'vrn': forms.TextInput(attrs={'class': 'form-control'}), 'rating': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0', 'max': '5'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } diff --git a/facility_management/management/__init__.py b/facility_management/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/facility_management/management/__pycache__/__init__.cpython-312.pyc b/facility_management/management/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..8632f74c Binary files /dev/null and b/facility_management/management/__pycache__/__init__.cpython-312.pyc differ diff --git a/facility_management/management/commands/__init__.py b/facility_management/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc b/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..7cf7a810 Binary files /dev/null and b/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc differ diff --git a/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc b/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc new file mode 100644 index 00000000..3ffcb7d5 Binary files /dev/null and b/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc differ diff --git a/facility_management/management/commands/seed_facility.py b/facility_management/management/commands/seed_facility.py new file mode 100644 index 00000000..23aaf388 --- /dev/null +++ b/facility_management/management/commands/seed_facility.py @@ -0,0 +1,633 @@ +# facility/management/commands/seed_facility_saudi.py +import random +import uuid +from datetime import datetime, timedelta, date +from decimal import Decimal + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth import get_user_model +from django.db import transaction +from django.utils import timezone + +# TODO: Change this import path to your app's models module +from facility_management.models import ( + Building, Floor, Room, + AssetCategory, Asset, + MaintenanceType, MaintenanceRequest, MaintenanceSchedule, + Vendor, ServiceContract, + Inspection, EnergyMeter, EnergyReading, + SpaceReservation, +) +from core.models import Tenant # Adjust if your Tenant lives elsewhere + + +SAUDI_CITIES = [ + ("Riyadh", 24.7136, 46.6753, [ + "Al Olaya", "Al Malqa", "Al Nakheel", "Al Sulaymaniyah", "Hittin", "Al Yasmin", "Al Wurud" + ]), + ("Jeddah", 21.4858, 39.1925, [ + "Al Rawdah", "Al Nahda", "Al Andalus", "Al Zahra", "Al Basateen", "Al Salama" + ]), + ("Dammam", 26.3927, 49.9777, [ + "Al Faisaliyah", "Al Shati", "Al Badiyah", "Al Mazruiyah", "An Nakheel" + ]), + ("Makkah", 21.3891, 39.8579, [ + "Al Awali", "Aziziyah", "Al Naseem", "Al Shara'i", "Al Kaakiya" + ]), + ("Madinah", 24.5247, 39.5692, [ + "Quba", "Qurban", "Al Khalidiyyah", "Al Azhari", "Al Rabwa" + ]), + ("Khobar", 26.2794, 50.2083, [ + "Al Aqrabiyah", "Al Khobar Al Shamaliyah", "Al Yarmouk", "Al Rakah" + ]), +] + +# Lightweight Saudi-flavored suppliers +VENDOR_NAMES = [ + "Saudi HVAC Co.", "Riyadh Fire Systems", "Gulf Electrical Services", + "Najd Facilities Cleaning", "Red Sea Security", "Eastern Landscaping", + "Hijaz Plumbing & Drainage", "Nusuk IT Services", "Al Haramain Maintenance" +] + +SERVICE_AREAS_SNIPPETS = [ + "Riyadh HQ campus, central plant and clinics", + "Jeddah airport satellite buildings and hangars", + "Dammam logistics & cold-storage facilities", + "Makkah accommodation & hospitality zones", + "Madinah clinical towers and outpatient blocks", +] + +ASSET_CATEGORIES = [ + ("HVAC Systems", "HVAC", "Chillers, AHUs, FCUs, pumps"), + ("Electrical", "ELEC", "Transformers, switchgear, UPS"), + ("Medical Equipment", "MED", "Imaging, patient monitors, ventilators"), + ("IT Equipment", "IT", "Servers, switches, wireless APs"), + ("Fire & Safety", "FIRE", "Fire pump, alarm panels, detectors"), + ("Plumbing", "PLB", "Pumps, tanks, RO/softeners"), +] + +MAINT_TYPES = [ + ("Preventive - HVAC", "PM_HVAC", "Quarterly PM for AHUs, filters, belts"), + ("Fire Alarm Test (SASO)", "FIRE_TEST", "Monthly fire alarm & pump tests"), + ("Elevator Inspection", "LIFT_INSP", "Quarterly inspection per standard"), + ("Water Quality Check", "WATER_QA", "Legionella sampling & flushing"), + ("Electrical IR Scan", "ELEC_IR", "Annual IR thermography of panels"), + ("Generator Load Test", "GEN_TEST", "Monthly DG set test under load"), +] + +ASSET_MODELS = [ + ("Trane", "RTAC-300", "CHILLER"), + ("Siemens", "SIVACON-S8", "SWITCHGEAR"), + ("Philips", "IntelliVue MX700", "PATIENT_MONITOR"), + ("Cisco", "Catalyst 9300", "SWITCH"), + ("Honeywell", "Notifier NFS2-3030", "FIRE_ALARM"), + ("Grundfos", "CR-32-10", "PUMP"), +] + +def rand_decimal(a, b, precision="0.01"): + q = Decimal(precision) + return (Decimal(a) + (Decimal(random.random()) * (Decimal(b) - Decimal(a)))).quantize(q) + +def rand_phone(): + # Saudi mobile format: 05x-xxxxxxx + return f"05{random.randint(0,9)}-{random.randint(1000000,9999999)}" + +def rand_email(name): + base = name.lower().replace(" ", ".") + return f"{base}@example.sa" + +def rand_crn(): + # KSA CRN often represented with 10 digits + return f"{random.randint(10**9, 10**10 - 1)}" + +def rand_vrn(): + # KSA VAT/TRN is 15 digits + return f"{random.randint(10**14, 10**15 - 1)}" + +def safe_choice(seq): + return random.choice(seq) if seq else None + +def ensure_users_for_tenant(tenant): + """ + Try to fetch existing users for the tenant; if not found, create a small set. + Works for custom AUTH_USER_MODEL that likely has a 'tenant' FK/field. + """ + User = get_user_model() + qs = User.objects.all() + # If your user model has 'tenant', filter by it. Otherwise, just use any user. + if hasattr(User, "tenant"): + qs = qs.filter(tenant=tenant) + + users = list(qs[:10]) + if users: + return users + + # Create a small staff set (adjust fields if your User requires more) + seed_names = [ + ("mohammed.fm", "Mohammed", "Al-Qahtani"), + ("ibrahim.bme", "Ibrahim", "Al-Otaibi"), + ("abdullah.main", "Abdullah", "Al-Zahrani"), + ("nora.admin", "Nora", "Al-Anazi"), + ("aisha.ops", "Aisha", "Al-Qurashi"), + ] + created = [] + for uname, first, last in seed_names: + kwargs = dict( + username=uname, + first_name=first, + last_name=last, + email=f"{uname}@tenhal.sa", + ) + if hasattr(User, "tenant"): + kwargs["tenant"] = tenant + try: + user = User.objects.create_user(**kwargs, password="Pass1234!") + except TypeError: + # Fallback if create_user signature differs + user = User.objects.create(**kwargs) + user.set_password("Pass1234!") + user.save() + created.append(user) + return created + +def unique_building_code(base, tenant_id): + # Make globally-unique code (your model has unique=True) + suffix = 1 + while True: + code = f"T{tenant_id}-{base}-{suffix:02d}" + if not Building.objects.filter(code=code).exists(): + return code + suffix += 1 + +def make_building(tenant, city_tuple, facility_manager): + city, lat, lng, districts = city_tuple + district = safe_choice(districts) + code_base = city.upper()[:3] + code = unique_building_code(code_base, tenant.id) + b = Building.objects.create( + tenant=tenant, + name=f"{city} {district or 'Campus'}", + code=code, + building_type=safe_choice([ct[0] for ct in Building.BuildingType.choices]), + address=f"{district or 'District'}, {city}, Saudi Arabia", + latitude=Decimal(f"{lat + random.uniform(-0.05, 0.05):.6f}"), + longitude=Decimal(f"{lng + random.uniform(-0.05, 0.05):.6f}"), + floor_count=random.randint(2, 10), + total_area_sqm=rand_decimal(5000, 80000), + construction_year=random.choice(range(1990, date.today().year + 1)), + architect="Dar Al Riyadh", + contractor="Saudi Binladin Group", + facility_manager=facility_manager, + ) + return b + +def make_floors_and_rooms(building, floors=5, rooms_per_floor=20): + floor_objs = [] + for n in range(floors): + f = Floor.objects.create( + building=building, + floor_number=n, + name=f"Level {n}", + area_sqm=rand_decimal(1000, 20000), + ceiling_height_m=rand_decimal(2.7, 4.2, "0.10"), + is_public_access=(n == 0), + ) + floor_objs.append(f) + room_objs = [] + for r in range(rooms_per_floor): + rn = f"{n:02d}{r+1:03d}" + room = Room.objects.create( + floor=f, + room_number=rn, + name=random.choice(["Clinic", "Office", "Storage", "Server", "Lab", "Ward", "Meeting"]), + area_sqm=rand_decimal(12, 120), + capacity=random.choice([None, 2, 4, 6, 10, 20]), + occupancy_status=safe_choice([s[0] for s in Room.OccupancyStatus.choices]), + is_accessible=True, + notes="" + ) + room_objs.append(room) + return floor_objs + +def seed_asset_categories(): + results = [] + for name, code, desc in ASSET_CATEGORIES: + obj, _ = AssetCategory.objects.get_or_create(code=code, defaults=dict(name=name, description=desc)) + if obj.name != name or obj.description != desc: + obj.name = name + obj.description = desc + obj.save() + results.append(obj) + # set some parents (simple tree) + hvac = next((c for c in results if c.code == "HVAC"), None) + fire = next((c for c in results if c.code == "FIRE"), None) + if fire and hvac and fire.parent_category_id is None: + fire.parent_category = hvac + fire.save() + return results + +def seed_maintenance_types(): + results = [] + for name, code, desc in MAINT_TYPES: + obj, _ = MaintenanceType.objects.get_or_create(code=code, defaults=dict(name=name, description=desc)) + if obj.name != name or obj.description != desc: + obj.name = name + obj.description = desc + obj.save() + results.append(obj) + return results + +def make_assets(buildings, categories, users, count=80): + results = [] + for _ in range(count): + building = safe_choice(buildings) + floor = safe_choice(list(Floor.objects.filter(building=building))) + room = safe_choice(list(Room.objects.filter(floor=floor))) if floor else None + manufacturer, model, _family = safe_choice(ASSET_MODELS) + + purchase_years_ago = random.randint(0, 12) + purchase_date = timezone.now().date() - timedelta(days=365 * purchase_years_ago + random.randint(0, 364)) + warranty_years = random.choice([1, 2, 3, 5]) + warranty_start = purchase_date + warranty_end = warranty_start + timedelta(days=365 * warranty_years) + + a = Asset.objects.create( + name=f"{manufacturer} {model}", + category=safe_choice(categories), + building=building, + floor=floor, + room=room, + location_description=f"Near {room.name if room else 'corridor'}", + manufacturer=manufacturer, + model=model, + serial_number=f"{manufacturer[:3].upper()}-{uuid.uuid4().hex[:10].upper()}", + purchase_date=purchase_date, + purchase_cost=rand_decimal(15000, 1500000, "1.00"), + current_value=rand_decimal(5000, 1200000, "1.00"), + depreciation_rate=Decimal("10.00"), + warranty_start_date=warranty_start, + warranty_end_date=warranty_end, + service_provider=safe_choice([v for v in VENDOR_NAMES]), + service_contract_number=f"SC-{timezone.now().year}-{random.randint(1000,9999)}", + status=safe_choice([s[0] for s in Asset.AssetStatus.choices]), + condition=safe_choice([c[0] for c in Asset.AssetCondition.choices]), + last_inspection_date=timezone.now().date() - timedelta(days=random.randint(5, 180)), + next_maintenance_date=timezone.now().date() + timedelta(days=random.randint(10, 120)), + assigned_to=safe_choice(users), + notes="" + ) + results.append(a) + return results + +def make_vendors(tenant, count=8): + results = [] + for i in range(count): + vname = VENDOR_NAMES[i % len(VENDOR_NAMES)] + contact = f"{vname.split()[0]} Rep" + obj, _ = Vendor.objects.get_or_create( + tenant=tenant, name=vname, + defaults=dict( + vendor_type=safe_choice([t[0] for t in Vendor.VendorType.choices]), + contact_person=contact, + email=rand_email(vname.replace(" ", "")), + phone=rand_phone(), + address=f"{safe_choice(SAUDI_CITIES)[0]} – {safe_choice(['Business Park','Industrial Area','Tech Valley'])}", + crn=rand_crn(), + vrn=rand_vrn(), + rating=rand_decimal("3.20", "4.90", "0.10"), + total_contracts=random.randint(1, 30), + is_active=True + ) + ) + results.append(obj) + return results + +def make_contracts(vendors, buildings, users, count=6): + results = [] + for _ in range(count): + ven = safe_choice(vendors) + start = timezone.now().date() - timedelta(days=random.randint(0, 400)) + end = start + timedelta(days=random.randint(180, 1095)) + cn = f"CTR-{ven.id}-{random.randint(1000,9999)}" + sc = ServiceContract.objects.create( + contract_number=cn, + vendor=ven, + title=f"{ven.name} – Comprehensive Service", + description="Preventive & corrective maintenance aligned with Vision 2030 reliability goals.", + start_date=start, + end_date=end, + contract_value=rand_decimal(100000, 5000000, "1.00"), + payment_terms="Net 30 days", + service_areas=safe_choice(SERVICE_AREAS_SNIPPETS), + status=safe_choice([c[0] for c in ServiceContract.ContractStatus.choices]), + auto_renewal=random.choice([True, False]), + renewal_notice_days=random.choice([30, 45, 60]), + contract_manager=safe_choice(users), + notes="" + ) + # Link to 1–3 buildings + sc.buildings.add(*random.sample(buildings, k=min(len(buildings), random.randint(1, 3)))) + results.append(sc) + return results + +def make_maintenance_requests(assets, mtypes, users, per_asset=1): + results = [] + for asset in assets: + for _ in range(per_asset): + req_by = safe_choice(users) + assigned = safe_choice(users) + mr = MaintenanceRequest.objects.create( + title=f"{asset.name} – {safe_choice(['Noise','Leak','Alarm','Performance drop','Vibration'])}", + description="Auto-generated test request reflecting typical O&M issues.", + maintenance_type=safe_choice(mtypes), + building=asset.building, + floor=asset.floor, + room=asset.room, + asset=asset, + priority=safe_choice([p[0] for p in MaintenanceRequest.Priority.choices]), + status=safe_choice([s[0] for s in MaintenanceRequest.MaintenanceStatus.choices]), + requested_by=req_by, + assigned_to=assigned if random.random() > 0.3 else None, + estimated_hours=rand_decimal("1.00", "16.00", "0.25"), + estimated_cost=rand_decimal(250, 10000, "1.00"), + actual_cost=None, + notes="", + completion_notes="" + ) + results.append(mr) + return results + +def make_schedules(assets, mtypes, users, count=60): + results = [] + combos = set() + for _ in range(count): + asset = safe_choice(assets) + building = asset.building + room = asset.room + key = (asset.id, building.id, room.id if room else None) + if key in combos: + continue + combos.add(key) + + freq = safe_choice([f[0] for f in MaintenanceSchedule.FrequencyInterval.choices]) + start = timezone.now().date() - timedelta(days=random.randint(0, 365)) + est = rand_decimal("1.00", "8.00", "0.25") + sched = MaintenanceSchedule.objects.create( + name=f"{asset.name} {safe_choice(['PM','Inspection','Service'])}", + description="Auto-generated PM program", + maintenance_type=safe_choice(mtypes), + asset=asset, + building=building, + room=room, + frequency_interval=freq, + start_date=start, + end_date=None, + assigned_to=safe_choice(users), + estimated_duration_hours=est, + is_active=True, + last_generated_date=None, + next_due_date=start + timedelta(days=freq), + ) + results.append(sched) + return results + +def make_inspections(buildings, users, count=20): + results = [] + for _ in range(count): + b = safe_choice(buildings) + scheduled = timezone.now() + timedelta(days=random.randint(-60, 60)) + insp = Inspection.objects.create( + inspection_id="", # let .save generate + inspection_type=safe_choice([t[0] for t in Inspection.InspectionType.choices]), + title=f"{b.code} – {safe_choice(['Quarterly','Annual','Random'])} Inspection", + description="Safety & compliance per local authority guidance.", + building=b, + scheduled_date=scheduled, + estimated_duration_hours=rand_decimal("1.00", "6.00", "0.25"), + inspector=safe_choice(users), + inspector_external=random.choice(["", "TUV Middle East", "Saudi Safety Org", "SASO Certifier"]), + inspector_organization=random.choice(["", "SASO", "Civil Defense", "Third-Party"]), + status=safe_choice([s[0] for s in Inspection.Status.choices]), + started_date=None, + completed_date=None, + overall_rating=random.choice(["", "Pass", "Fail", "Conditional"]), + findings="", + recommendations="", + requires_followup=random.choice([True, False]), + followup_date=None, + ) + # relate a few floors/rooms/assets + floors = list(Floor.objects.filter(building=b)) + if floors: + insp.floors.add(*random.sample(floors, k=min(len(floors), random.randint(1, 3)))) + rooms = list(Room.objects.filter(floor__in=floors)) + if rooms: + insp.rooms.add(*random.sample(rooms, k=min(len(rooms), random.randint(1, 5)))) + assets = list(Asset.objects.filter(building=b)) + if assets: + insp.assets.add(*random.sample(assets, k=min(len(assets), random.randint(1, 5)))) + results.append(insp) + return results + +def make_energy_meters(buildings): + """ + Create 2–4 meters per building (electricity + water + optional others) + """ + results = [] + meter_types = [EnergyMeter.MeterType.ELECTRICITY, EnergyMeter.MeterType.WATER, + EnergyMeter.MeterType.GAS, EnergyMeter.MeterType.CHILLED_WATER] + for b in buildings: + for mt in random.sample(meter_types, k=random.randint(2, 4)): + meter_id = f"MTR-{b.code}-{mt[:2]}-{random.randint(1000,9999)}" + em = EnergyMeter.objects.create( + meter_id=meter_id, + meter_type=mt, + building=b, + location_description=f"Utility Room – {safe_choice(['A','B','C'])}", + manufacturer=safe_choice(["Schneider", "Siemens", "ABB", "Yokogawa"]), + model=f"{safe_choice(['Q1','E45','HX','Prime'])}-{random.randint(100,999)}", + serial_number=uuid.uuid4().hex[:12].upper(), + installation_date=timezone.now().date() - timedelta(days=random.randint(100, 2000)), + current_reading=Decimal("0.00"), + last_reading_date=None, + is_active=True, + calibration_date=timezone.now().date() - timedelta(days=random.randint(100, 800)), + next_calibration_date=timezone.now().date() + timedelta(days=random.randint(100, 800)), + ) + results.append(em) + return results + +def make_energy_readings(meters, days=60, users=None): + """ + Generate monotonic readings over past N days. Electricity in kWh, water in m3, etc. + """ + results = [] + users = users or [] + reader = safe_choice(users) + for m in meters: + base = rand_decimal(1000, 50000, "1.00") + daily_incr = rand_decimal(50, 1500, "1.00") + ts = timezone.now() - timedelta(days=days) + for d in range(days): + ts += timedelta(days=1) + base += daily_incr + rand_decimal(-0.20, 0.20, "0.01") * daily_incr # mild variation + er = EnergyReading.objects.create( + meter=m, + reading_date=ts, + reading_value=base.quantize(Decimal("1.00")), + consumption=None, # model .save() derives consumption + cost=None, + read_by=reader, + is_estimated=random.random() < 0.1, + notes="" + ) + results.append(er) + return results + +def make_reservations(rooms, users, count=50): + results = [] + for _ in range(count): + room = safe_choice(rooms) + reserved_by = safe_choice(users) + start = timezone.now() + timedelta(days=random.randint(-15, 30), hours=random.randint(8, 17)) + duration_h = random.choice([1, 2, 3, 4, 6, 8]) + end = start + timedelta(hours=duration_h) + hourly_rate = random.choice([None, Decimal("150.00"), Decimal("300.00")]) + + sr = SpaceReservation.objects.create( + reservation_id="", # let .save generate + room=room, + title=f"{safe_choice(['Training', 'Meeting', 'Workshop', 'Orientation'])} – {room.name}", + description=f"Auto-generated booking for {room.floor.building.code}", + start_datetime=start, + end_datetime=end, + reserved_by=reserved_by, + contact_person=f"{reserved_by.first_name or 'Contact'} {reserved_by.last_name or ''}".strip(), + contact_email=reserved_by.email or rand_email("contact"), + contact_phone=rand_phone(), + expected_attendees=random.randint(3, 30), + setup_requirements=random.choice(["Theater", "U-Shape", "Classroom", "Boardroom"]), + catering_required=random.random() < 0.3, + av_equipment_required=random.random() < 0.6, + status=safe_choice([s[0] for s in SpaceReservation.ReservationStatus.choices]), + approved_by=safe_choice(users) if random.random() < 0.7 else None, + approved_at=timezone.now() if random.random() < 0.7 else None, + hourly_rate=hourly_rate, + total_cost=None, # model .save() computes if hourly_rate is provided + notes="" + ) + results.append(sr) + return results + + +class Command(BaseCommand): + help = ( + "Seed Saudi-influenced, multi-tenant facility data (buildings, floors/rooms, assets, " + "maintenance, vendors/contracts, inspections, meters/readings, reservations).\n\n" + "Examples:\n" + " python manage.py seed_facility_saudi --tenant 1\n" + " python manage.py seed_facility_saudi --tenant 1 --buildings 3 --floors 6 --rooms-per-floor 25 --assets 120 --seed 42 --purge\n" + ) + + def add_arguments(self, parser): + parser.add_argument("--tenant", required=True, help="Tenant ID or slug.") + parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility.") + parser.add_argument("--buildings", type=int, default=4) + parser.add_argument("--floors", type=int, default=5) + parser.add_argument("--rooms-per-floor", type=int, default=20) + parser.add_argument("--assets", type=int, default=80) + parser.add_argument("--purge", action="store_true", help="Delete existing tenant data first.") + + def handle(self, *args, **opts): + if opts["seed"] is not None: + random.seed(opts["seed"]) + + tenant = self._get_tenant(opts["tenant"]) + if not tenant: + raise CommandError("Tenant not found.") + + User = get_user_model() + users = ensure_users_for_tenant(tenant) + + building_count = max(1, opts["buildings"]) + floors = max(1, opts["floors"]) + rpf = max(5, opts["rooms_per_floor"]) + asset_target = max(10, opts["assets"]) + + with transaction.atomic(): + if opts["purge"]: + self._purge_tenant_data(tenant) + self.stdout.write(self.style.WARNING(f"Purged existing data for tenant {tenant}.")) + + # Seed lookup tables + categories = seed_asset_categories() + mtypes = seed_maintenance_types() + + # Buildings, floors, rooms + buildings = [] + for i in range(building_count): + city_tuple = safe_choice(SAUDI_CITIES) + fm = safe_choice(users) + b = make_building(tenant, city_tuple, fm) + buildings.append(b) + make_floors_and_rooms(b, floors=floors, rooms_per_floor=rpf) + + # Vendors & contracts + vendors = make_vendors(tenant, count=min(10, len(VENDOR_NAMES))) + contracts = make_contracts(vendors, buildings, users, count=6) + + # Assets + assets = make_assets(buildings, categories, users, count=asset_target) + + # Maintenance + mreqs = make_maintenance_requests(assets, mtypes, users, per_asset=1) + msched = make_schedules(assets, mtypes, users, count=min(60, len(assets))) + + # Inspections + insps = make_inspections(buildings, users, count=20) + + # Energy meters and readings + meters = make_energy_meters(buildings) + mreads = make_energy_readings(meters, days=60, users=users) + + # Reservations + rooms = list(Room.objects.filter(floor__building__in=buildings)) + resvs = make_reservations(rooms, users, count=50) + + self.stdout.write(self.style.SUCCESS( + f"Seed complete for tenant {tenant}:\n" + f" Buildings: {len(buildings)} (Floors x Rooms: {floors} x {rpf})\n" + f" Vendors: {len(vendors)} | Contracts: {len(contracts)}\n" + f" Asset Categories: {len(categories)} | Assets: {len(assets)}\n" + f" Maint. Types: {len(mtypes)} | Requests: {len(mreqs)} | Schedules: {len(msched)}\n" + f" Inspections: {len(insps)}\n" + f" Energy Meters: {len(meters)} | Readings: {len(mreads)}\n" + f" Reservations: {len(resvs)}" + )) + + def _get_tenant(self, ident_or_slug): + # Try pk then slug + try: + return Tenant.objects.get(pk=int(ident_or_slug)) + except (Tenant.DoesNotExist, ValueError): + pass + try: + return Tenant.objects.get(slug=ident_or_slug) + except Tenant.DoesNotExist: + return None + + def _purge_tenant_data(self, tenant): + # Order matters due to FK relations + SpaceReservation.objects.filter(room__floor__building__tenant=tenant).delete() + EnergyReading.objects.filter(meter__building__tenant=tenant).delete() + EnergyMeter.objects.filter(building__tenant=tenant).delete() + Inspection.objects.filter(building__tenant=tenant).delete() + MaintenanceSchedule.objects.filter(building__tenant=tenant).delete() + MaintenanceRequest.objects.filter(building__tenant=tenant).delete() + Asset.objects.filter(building__tenant=tenant).delete() + ServiceContract.objects.filter(buildings__tenant=tenant).delete() + Vendor.objects.filter(tenant=tenant).delete() + Room.objects.filter(floor__building__tenant=tenant).delete() + Floor.objects.filter(building__tenant=tenant).delete() + Building.objects.filter(tenant=tenant).delete() \ No newline at end of file diff --git a/facility_management/models.py b/facility_management/models.py index 932b510d..6a06289f 100644 --- a/facility_management/models.py +++ b/facility_management/models.py @@ -61,7 +61,7 @@ class Building(models.Model): return 0 occupied_rooms = Room.objects.filter( floor__building=self, - occupancy_status='occupied' + occupancy_status=Room.OccupancyStatus.OCCUPIED ).count() return (occupied_rooms / total_rooms) * 100 diff --git a/facility_management/templates/facility_management/maintenance/list.html b/facility_management/templates/facility_management/maintenance/list.html index 3d642d8f..9d677899 100644 --- a/facility_management/templates/facility_management/maintenance/list.html +++ b/facility_management/templates/facility_management/maintenance/list.html @@ -268,7 +268,7 @@ {% if is_paginated %} - {% include 'pagination.html' %} + {% include 'partial/pagination.html' %} {% endif %} {% else %}
|---|