Marwan Alwali 94e180f776 update
2025-09-21 17:38:18 +03:00

633 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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