class BreakItem(LegItem): item_type = 'Break' paid = Time(0) unpaid = Time(0) split_shift = False def __init__(self, prev_job, next_job): super().__init__(prev_job, next_job) self.paid = self.total_time def output(self): return [self.item_type, self.start_time.to_time(), '-', '-', self.finish_time.to_time(), None, self.paid.to_time() if self.paid.seconds else None, self.unpaid.to_time() if self.unpaid.seconds else None, self.paid.to_time() if self.paid.seconds else None]
def check_max_working_time(self): idx = 0 min_idx = 0 items = self.items[:] time_worked = Time(0) while(items): item = items.pop(0) idx += 1 if isinstance(item, BreakItem): time_worked = Time(0) min_idx = idx else: time_worked += item.total_time if time_worked > settings.MAX_TIME_BEFORE_BREAK: # greedy grab all jobs until the next break if items and not isinstance(item, BreakItem): continue alert = ExceedFatigueMgmtAlert(item, time_worked) self.items.insert(idx, alert) for item in self.items[min_idx:idx + 1]: item.colour = settings.BREAK_ALERT_COLOUR return
def load_jobs(path: str) -> list: # read from xlsx spreadsheet workbook = xlrd.open_workbook(path) sheet = workbook.sheet_by_index(0) jobs = [] for index in range(2, sheet.nrows): row = sheet.row_values(index) driver_code, driver_name, signon_time, start_time, pickup_place, dest_place, _, finish_time, signoff_time, _, pickup_lat, pickup_long, dest_lat, dest_long, *_ = row # header rows if driver_code.startswith('Coach Manager') or driver_code.startswith( 'Driver') or driver_code.startswith( 'Record Count') or driver_code.startswith('WHERE ('): continue # empy jobs if not driver_code.strip(): continue # job sign on and signoff times try: signon_time = Time(signon_time) except ValueError: raise ParserException('Cannot convert sign on time', row, index + 1) try: signoff_time = Time(signoff_time) except ValueError: raise ParserException('Cannot convert sign off time', row, index + 1) # pickup try: start_time = Time(start_time) except ValueError: raise ParserException('Cannot convert start time', row, index + 1) pickup_location = mapping.Location( pickup_place.strip(), start_time, mapping.GPS(pickup_lat, pickup_long)) # destination try: finish_time = Time(finish_time) except ValueError: raise ParserException('Cannot convert finish time', row, index + 1) dest_location = mapping.Location(dest_place.strip(), finish_time, mapping.GPS(dest_lat, dest_long)) # check for weird times if finish_time < start_time: raise TimeException('Finish time cannot be before start time', row, index + 1) # job driver = Driver.get_driver(driver_code, driver_name) job = Job(pickup_location, dest_location, signon_time, signoff_time) driver.add_job(job) jobs.append(job) return jobs
def __init__(self, prev_job, next_job): super().__init__(prev_job, next_job) self.paid = Time(0) self.unpaid = self.total_time
def process(self): breaks = [] split_shift = False prev_job = self.jobs.pop(0) if prev_job.pickup.place != 'Depot': self.items.append(FromDepotItem(prev_job)) self.items.append(JobItem(prev_job)) while self.jobs: next_job = self.jobs.pop(0) # split shift if next_job.signon_time - prev_job.signoff_time > settings.SPLIT_SHIFT_THRESHOLD: to_depot = ToDepotItem(prev_job) from_depot = FromDepotItem(next_job) split_shift = SplitShiftItem(to_depot, from_depot) self.items.append(to_depot) self.items.append(split_shift) self.items.append(from_depot) breaks.append(split_shift) split_shift = True else: # repositioning if prev_job.destination != next_job.pickup: item = PositioningItem(prev_job, next_job) if item.is_material: self.items.append(item) # break last_item = self.items[-1] if last_item.finish_time < next_job.start_time: break_ = BreakItem(last_item, next_job) self.items.append(break_) breaks.append(break_) self.items.append(JobItem(next_job)) prev_job = next_job if prev_job.pickup.place != 'Depot': self.items.append(ToDepotItem(prev_job)) # appply EBA breaks = list(reversed(sorted(breaks, key=operator.attrgetter('total_time')))) if breaks: # if split shift - all other breaks are paid if split_shift: breaks.pop(0) for break_ in breaks: break_.paid = break_.total_time break_.unpaid = Time(0) else: breaks = list(filter(lambda item: item.total_time >= settings.MIN_BREAK_TIME, breaks)) blocks = [] for break_ in breaks: blocks.extend(self.break_blocks(break_)) # sort the blocks by size and take top BREAK_COUNT blocks to be unpaid blocks = sorted(blocks, key=operator.itemgetter(1)) unpaid_blocks = blocks[-settings.BREAK_COUNT:] for break_, block in unpaid_blocks: break_.unpaid += Time(block) break_.paid -= Time(block) # generate shifts split_shift_finder = lambda item: isinstance(item, SplitShiftItem) and item.unpaid and not item.paid shifts = split(self.items, split_shift_finder) self.shifts = [Shift(item, len(shifts) > 1) for item in shifts] if len(shifts) > 1: self.split = first(self.items, split_shift_finder) # check minimum total shift time elif self.total_payable_hours < settings.MIN_SHIFT_DURATION: time = Time(settings.MIN_SHIFT_DURATION - self.total_payable_hours.seconds) adjustment = MinimumShiftAdjustment(self.shifts[0].items[-1], time) self.shifts[0].items.append(adjustment)
def total_adjustments(self): time = Time(sum([item.total_adjustments for item in self.shifts])) return time
def total_unpaid_breaks(self): breaks = Time(sum([shift.total_unpaid_breaks.seconds for shift in self.shifts])) if self.split: breaks += self.split.total_time return breaks
def total_paid_breaks(self): return Time(sum([shift.total_paid_breaks.seconds for shift in self.shifts]))
def adjustment(self): return Time(settings.MIN_SPLIT_DURATION - self.total_payable_hours.seconds if self.split else settings.MIN_SHIFT_DURATION - self.total_payable_hours.seconds)
def total_adjustments(self): time = Time(sum([item.total_time for item in self.items if isinstance(item, Adjustment)])) return time
def total_unpaid_breaks(self): return Time(sum([item.unpaid.seconds for item in self.items if isinstance(item, BreakItem)]))
def total_time(self): return Time(self.duration)
def total_time(self): return Time(0)