633 lines
26 KiB
Python
633 lines
26 KiB
Python
# 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() |