420 lines
15 KiB
Python
420 lines
15 KiB
Python
from datetime import datetime
|
|
|
|
import phonenumbers
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
|
|
|
|
class Setting(models.Model):
|
|
name = models.CharField(max_length=20, unique=True)
|
|
value = models.CharField(max_length=50, blank=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Suburb(models.Model):
|
|
STATE = [
|
|
("VIC", "Victoria"),
|
|
("NSW", "New South Wales"),
|
|
("SA", "South Australia"),
|
|
("ACT", "Australia Capital Territory"),
|
|
("QLD", "Queensland"),
|
|
("NT", "Northern Territory"),
|
|
("WA", "Western Australia"),
|
|
("TAS", "Tasmania"),
|
|
]
|
|
name = models.CharField(max_length=30, unique=True)
|
|
state = models.CharField(max_length=3, choices=STATE)
|
|
postcode = models.PositiveSmallIntegerField()
|
|
distance = models.PositiveSmallIntegerField(blank=True, null=True)
|
|
|
|
class Meta:
|
|
ordering = ["name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.name}, {self.state} {self.postcode}"
|
|
|
|
|
|
class School(models.Model):
|
|
name = models.CharField(max_length=30, unique=True)
|
|
shortName = models.CharField(max_length=10, unique=True)
|
|
address = models.CharField(max_length=50)
|
|
suburb = models.ForeignKey(Suburb, on_delete=models.CASCADE)
|
|
email = models.CharField(max_length=50, blank=True)
|
|
phone = models.CharField(max_length=15, blank=True)
|
|
principal_name = models.CharField(max_length=50, blank=True)
|
|
principal_phone = models.CharField(max_length=15, blank=True)
|
|
notes = models.TextField(blank=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Company(models.Model):
|
|
name = models.CharField(max_length=50, unique=True)
|
|
contact_name = models.CharField(max_length=50, blank=True)
|
|
contact_number = models.CharField(max_length=15, blank=True)
|
|
contact_mobile = models.CharField(max_length=15, blank=True)
|
|
contact_email = models.CharField(max_length=50, blank=True)
|
|
address = models.CharField(max_length=50, blank=True)
|
|
suburb = models.ForeignKey(Suburb, on_delete=models.CASCADE)
|
|
notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
verbose_name_plural = "Companies"
|
|
|
|
def __str__(self):
|
|
return f"{self.name}"
|
|
|
|
|
|
class Bus(models.Model):
|
|
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
|
route_name = models.CharField(max_length=50, unique=True)
|
|
contract_number = models.CharField(max_length=20, blank=True)
|
|
registration = models.CharField(max_length=10, blank=True)
|
|
seating_capacity = models.SmallIntegerField()
|
|
make = models.CharField(max_length=15, blank=True)
|
|
model = models.CharField(max_length=15, blank=True)
|
|
notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
verbose_name_plural = "Buses"
|
|
ordering = ["route_name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.route_name}"
|
|
|
|
def traveller_count(self, date=None):
|
|
count = 0
|
|
for traveller in Traveller.objects.filter(bus_stops__bus=self):
|
|
if traveller._is_active(date):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
class Shuttle(models.Model):
|
|
bus = models.ForeignKey(Bus, on_delete=models.CASCADE)
|
|
school = models.ForeignKey(School, on_delete=models.CASCADE)
|
|
custom_name = models.CharField(max_length=10, blank=True)
|
|
# transfer_school = models.ForeignKey(School, related_name='transfer_school', on_delete=models.CASCADE)
|
|
am_service = models.BooleanField(default=True)
|
|
pm_service = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ["school__name"]
|
|
|
|
def __str__(self):
|
|
custom_name = self.custom_name
|
|
if custom_name:
|
|
custom_name = f" ({self.custom_name})"
|
|
else:
|
|
custom_name = ""
|
|
return f"{self.school.shortName} <-> {self.bus.route_name}{custom_name}"
|
|
|
|
def traveller_count(self, date=None):
|
|
count = 0
|
|
for traveller in Traveller.objects.filter(shuttle=self):
|
|
if traveller._is_active(date):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
class Driver(models.Model):
|
|
bus = models.ForeignKey(Bus, on_delete=models.CASCADE)
|
|
first_name = models.CharField(max_length=50)
|
|
last_name = models.CharField(max_length=50)
|
|
phone_number = models.CharField(max_length=15, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ["last_name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
|
|
class BusStop(models.Model):
|
|
bus = models.ForeignKey(Bus, on_delete=models.CASCADE)
|
|
am_time = models.TimeField()
|
|
pm_time = models.TimeField()
|
|
address = models.CharField(max_length=100)
|
|
notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
ordering = ["bus__route_name", "am_time"]
|
|
|
|
def get_stop_number(self):
|
|
return BusStop.objects.filter(bus=self.bus, am_time__lt=self.am_time).count() + 1
|
|
|
|
def __str__(self):
|
|
return f"{self.bus.route_name} #{self.get_stop_number()} - {self.address}"
|
|
|
|
|
|
class Traveller(models.Model):
|
|
YEAR = [
|
|
("PS", "PreSchool"),
|
|
("00", "Year 00"),
|
|
("01", "Year 01"),
|
|
("02", "Year 02"),
|
|
("03", "Year 03"),
|
|
("04", "Year 04"),
|
|
("05", "Year 05"),
|
|
("06", "Year 06"),
|
|
("07", "Year 07"),
|
|
("08", "Year 08"),
|
|
("09", "Year 09"),
|
|
("10", "Year 10"),
|
|
("11", "Year 11"),
|
|
("12", "Year 12"),
|
|
("AL", "Adult Learner"),
|
|
]
|
|
|
|
ELIGIBILITY_STATUS = [
|
|
("1", "Eligible"),
|
|
("2", "Ineligible"),
|
|
("3", "<4.8 Exemption"),
|
|
("4", "Eligible waitlisted"),
|
|
("5", "Ineligible waitlisted"),
|
|
("6", "Kinder Exemption"),
|
|
("7", "Tafe/Post Secondary Exemption"),
|
|
("8", "Other Exemption"),
|
|
]
|
|
|
|
school = models.ForeignKey(School, on_delete=models.PROTECT)
|
|
first_name = models.CharField(max_length=50)
|
|
last_name = models.CharField(max_length=50)
|
|
dob = models.DateField(blank=True, null=True)
|
|
year_level = models.CharField(max_length=2, choices=YEAR)
|
|
bus_stops = models.ManyToManyField(BusStop, through='TravellerRoute', blank=True)
|
|
distance_to_school = models.PositiveSmallIntegerField(blank=True, null=True)
|
|
address = models.CharField(max_length=100, default="")
|
|
|
|
travel_start_date = models.DateField(blank=True, null=True)
|
|
travel_end_date = models.DateField(blank=True, null=True)
|
|
eligibility_status = models.CharField(max_length=1, choices=ELIGIBILITY_STATUS)
|
|
assessment_date = models.DateField(blank=True, null=True)
|
|
fee_per_term = models.DecimalField(decimal_places=2, max_digits=5, blank=True, null=True)
|
|
term_1_paid = models.BooleanField(default=False)
|
|
term_2_paid = models.BooleanField(default=False)
|
|
term_3_paid = models.BooleanField(default=False)
|
|
term_4_paid = models.BooleanField(default=False)
|
|
application_form_completed = models.BooleanField()
|
|
parent_notified = models.BooleanField()
|
|
seat_number = models.CharField(max_length=5, blank=True)
|
|
|
|
created_on = models.DateTimeField(auto_now_add=True, blank=True, null=True)
|
|
last_edit = models.DateTimeField(auto_now=True, blank=True, null=True)
|
|
is_archived = models.BooleanField(default=False, verbose_name="Archived")
|
|
is_active = models.BooleanField(default=False, verbose_name='Active')
|
|
notes = models.TextField(blank=True, verbose_name='Admin Notes')
|
|
shuttle = models.ForeignKey(Shuttle, on_delete=models.SET_NULL, blank=True, null=True)
|
|
|
|
class Meta:
|
|
ordering = ["last_name", "first_name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
self._update_active_status()
|
|
self._repopulate_address()
|
|
super(Traveller, self).save(*args, **kwargs)
|
|
|
|
def _is_active(self, date=None):
|
|
if date is None:
|
|
today = datetime.today()
|
|
date = datetime(today.year, today.month, today.day)
|
|
|
|
if not self.travel_start_date or datetime(self.travel_start_date.year, self.travel_start_date.month, self.travel_start_date.day) > date:
|
|
return False
|
|
if not self.travel_end_date:
|
|
return True
|
|
|
|
end_date = datetime(self.travel_end_date.year, self.travel_end_date.month, self.travel_end_date.day)
|
|
return end_date >= date
|
|
|
|
def _update_active_status(self):
|
|
new_start_date = None
|
|
new_end_date = None
|
|
|
|
for travellerRoute in TravellerRoute.objects.filter(traveller=self.id):
|
|
route_start = travellerRoute.travel_start_date
|
|
route_end = travellerRoute.travel_end_date
|
|
if route_start is not None:
|
|
if new_start_date is None or new_start_date > route_start:
|
|
new_start_date = route_start
|
|
if route_end is not None:
|
|
if new_end_date is None or new_end_date < route_end:
|
|
new_end_date = route_end
|
|
|
|
self.travel_start_date = new_start_date
|
|
self.travel_end_date = new_end_date
|
|
self.is_active = self._is_active()
|
|
|
|
def fare_paying(self):
|
|
if self.eligibility_status != "2":
|
|
return
|
|
cost_setting = Setting.objects.filter(name="TERM_TRAVEL_COST")
|
|
|
|
if not cost_setting.exists():
|
|
return "TERM_TRAVEL_COST not configured"
|
|
|
|
cost = int(cost_setting.get().value)
|
|
|
|
stops = 0
|
|
for stop in TravellerRoute.objects.filter(traveller=self.id):
|
|
stops += stop.active_stops()
|
|
if stops > 1:
|
|
stops = 1
|
|
return f"${round(cost*stops)}"
|
|
|
|
def _repopulate_address(self):
|
|
families = self.get_families()
|
|
if families.count() == 0:
|
|
self.address = ""
|
|
elif families.count() == 1:
|
|
family = families.first()
|
|
self.address = f"{family.residential_address} {family.residential_suburb}"
|
|
else:
|
|
self.address = "Multiple"
|
|
|
|
def get_families(self):
|
|
return Family.objects.filter(traveller__id__exact=self.id)
|
|
|
|
|
|
class Family(models.Model):
|
|
RELATIONS = [
|
|
("1", "Parent"),
|
|
("2", "Step-Parent"),
|
|
("3", "Foster Parent"),
|
|
("4", "Host Family"),
|
|
("5", "Sibling"),
|
|
("6", "Grandparent"),
|
|
("7", "Aunt/Uncle"),
|
|
("8", "Cousin"),
|
|
("9", "Carer"),
|
|
("10", "Case Worker"),
|
|
("11", "Friend/Other"),
|
|
]
|
|
|
|
traveller = models.ForeignKey(Traveller, on_delete=models.CASCADE)
|
|
residential_address = models.CharField(max_length=50, blank=True)
|
|
residential_suburb = models.ForeignKey(Suburb, on_delete=models.PROTECT, blank=True, null=True,
|
|
related_name='family_residential_suburb')
|
|
postal_address = models.CharField(max_length=50, blank=True)
|
|
postal_suburb = models.ForeignKey(Suburb, on_delete=models.PROTECT, blank=True, null=True,
|
|
related_name='family_postal_suburb')
|
|
parent_A_firstname = models.CharField(max_length=50, blank=True)
|
|
parent_A_lastname = models.CharField(max_length=50, blank=True)
|
|
parent_A_phone = models.CharField(max_length=15, blank=True)
|
|
parent_A_email = models.CharField(max_length=50, blank=True)
|
|
parent_B_firstname = models.CharField(max_length=50, blank=True)
|
|
parent_B_lastname = models.CharField(max_length=50, blank=True)
|
|
parent_B_phone = models.CharField(max_length=15, blank=True)
|
|
parent_B_email = models.CharField(max_length=50, blank=True)
|
|
emergency_contact_A_firstname = models.CharField(max_length=50, blank=True)
|
|
emergency_contact_A_lastname = models.CharField(max_length=50, blank=True)
|
|
emergency_contact_A_phone = models.CharField(max_length=15, blank=True)
|
|
emergency_contact_A_relation = models.CharField(max_length=50, choices=RELATIONS, blank=True)
|
|
emergency_contact_B_firstname = models.CharField(max_length=50, blank=True)
|
|
emergency_contact_B_lastname = models.CharField(max_length=50, blank=True)
|
|
emergency_contact_B_phone = models.CharField(max_length=15, blank=True)
|
|
emergency_contact_B_relation = models.CharField(max_length=50, choices=RELATIONS, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name_plural = "Families"
|
|
|
|
def __str__(self):
|
|
return self.parent_names()
|
|
|
|
def clean(self):
|
|
valid_numbers, failed_numbers = self.get_parsed_numbers(True, True)
|
|
if len(failed_numbers) > 0:
|
|
raise ValidationError(f"Phone number {failed_numbers[0]} not valid")
|
|
|
|
def parent_names(self):
|
|
a_name = self.parent_A_firstname
|
|
b_name = self.parent_B_firstname
|
|
if a_name:
|
|
if b_name:
|
|
return f"{a_name} and {b_name}"
|
|
return a_name
|
|
elif b_name:
|
|
return b_name
|
|
return ""
|
|
|
|
def get_parsed_numbers(self, parents=False, emergency=False):
|
|
numbers = []
|
|
if parents:
|
|
if self.parent_A_phone:
|
|
numbers.append(self.parent_A_phone)
|
|
if self.parent_B_phone:
|
|
numbers.append(self.parent_B_phone)
|
|
if emergency:
|
|
if self.emergency_contact_A_phone:
|
|
numbers.append(self.emergency_contact_A_phone)
|
|
if self.emergency_contact_B_phone:
|
|
numbers.append(self.emergency_contact_B_phone)
|
|
valid_numbers = []
|
|
failed_numbers = []
|
|
for number in numbers:
|
|
try:
|
|
num = phonenumbers.parse(number, "AU")
|
|
if phonenumbers.is_valid_number(num):
|
|
valid_numbers.append(f"+{num.country_code}{num.national_number}")
|
|
else:
|
|
failed_numbers.append(number)
|
|
except:
|
|
failed_numbers.append(number)
|
|
|
|
return valid_numbers, failed_numbers
|
|
|
|
|
|
class TravellerRoute(models.Model):
|
|
traveller = models.ForeignKey(Traveller, on_delete=models.CASCADE)
|
|
busStop = models.ForeignKey(BusStop, on_delete=models.CASCADE)
|
|
travel_start_date = models.DateField(blank=True, null=True)
|
|
travel_end_date = models.DateField(blank=True, null=True)
|
|
mon_am = models.BooleanField(default=True)
|
|
mon_pm = models.BooleanField(default=True)
|
|
tue_am = models.BooleanField(default=True)
|
|
tue_pm = models.BooleanField(default=True)
|
|
wen_am = models.BooleanField(default=True)
|
|
wen_pm = models.BooleanField(default=True)
|
|
thu_am = models.BooleanField(default=True)
|
|
thu_pm = models.BooleanField(default=True)
|
|
fri_am = models.BooleanField(default=True)
|
|
fri_pm = models.BooleanField(default=True)
|
|
notes = models.TextField(blank=True, verbose_name="Driver Notes")
|
|
|
|
def __str__(self):
|
|
return f"{self.busStop}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
super(TravellerRoute, self).save(*args, **kwargs)
|
|
self.traveller.save()
|
|
|
|
def active_stops(self):
|
|
stops = 0
|
|
if self.mon_am:
|
|
stops += 1
|
|
if self.mon_pm:
|
|
stops += 1
|
|
if self.tue_am:
|
|
stops += 1
|
|
if self.tue_pm:
|
|
stops += 1
|
|
if self.wen_am:
|
|
stops += 1
|
|
if self.wen_pm:
|
|
stops += 1
|
|
if self.thu_am:
|
|
stops += 1
|
|
if self.thu_pm:
|
|
stops += 1
|
|
if self.fri_am:
|
|
stops += 1
|
|
if self.fri_pm:
|
|
stops += 1
|
|
return stops * 0.1
|