# 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.db import transaction from django.utils import timezone # TODO: Change this import path to your app's models module from facility_management.models import * from core.models import Tenant # Adjust if your Tenant lives elsewhere from accounts.models import User 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 = settings.AUTH_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=uuid.uuid4(), # 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 = settings.AUTH_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()