# 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()