def _create_order_item(self, item, order): master_id = item['master_id'] logger.info(f'Creating order_item for order={order.id}, ' f'master_id={master_id}') master = Master.objects.prefetch_related('schedule').get( pk=master_id) services = Service.objects.filter(pk__in=item['service_ids']) if not services: raise ValidationError( f'Services with provided ids:{item["service_ids"]} ' f'are not found') schedule = master.get_schedule(order.date) order_item = None start_time = None for service in services: start_time = start_time or order.time next_slot_time = add_time(start_time, minutes=service.max_duration) # master's schedule could have changed after the search if not time_slot_utils.service_fits_into_slots( service, schedule.time_slots.all(), start_time, next_slot_time): raise ApplicationError( 'Unable to create order. ' 'Master\'s schedule has changed ', error_type=ApplicationError.ErrorTypes. ORDER_CREATION_ERROR) order_item = OrderItem.objects.create(order=order, master=master, service=service, locked=item['locked']) master.create_order_payment(order, order_item) logger.info(f'Filling time_slots for ' f'master={master.first_name}, ' f'service={service.name}, ' f'duration={service.max_duration}, ' f'schedule_date={schedule.date}') start_time = schedule.assign_time( start_time, next_slot_time, order_item) # add +1 if it's not end of the day if start_time: next_slot_time = add_time(start_time, minutes=TimeSlot.DURATION) schedule.assign_time(start_time, next_slot_time, order_item=order_item)
def assign_time(self, start_time: datetime.time, end_time: datetime.time, order_item=None, taken=True): """ Marks slots between `start_time` and `end_time` as Taken. `end_time` is excluded :param order_item: :param start_time: :param end_time: :param taken: :return: time <datetime> of the next available time slot or None if the last processed slot marks the end of the work day """ from src.apps.masters import time_slot_utils if not start_time: raise ValueError('start_time argument should not be None') logging.info(f'Setting slots between [{start_time},{end_time}] ' f'to Taken') time_slots = sorted(self.time_slots.all(), key=lambda slot: slot.value) # looking for the first timeslot for first_slot_index, time_slot in enumerate(time_slots): if time_slot.value == start_time: break else: raise ValueError('time not found') # TODO index error # TODO this method blows, potential performance issues shift = 0 cur_time = start_time while cur_time < end_time: ts = self.time_slots.get(pk=time_slots[first_slot_index + shift].id) ts.taken = taken ts.order_item = order_item ts.save() cur_time = time_slot_utils.add_time(cur_time, minutes=TimeSlot.DURATION) shift += 1 next_index = first_slot_index + shift if next_index == len(time_slots): logging.info(f'No next slot, last slot of the day is filled') return None return time_slots[next_index].value
def datetime(masters: Iterable[Master], params: FilteringParams): """ Returns a list of masters who can do the specific service at specific date and time for the specific client, taking into account the possibility to get to the client :param masters: masters to filter :param params: :return: """ service_ids = params.services date = params.date time = params.time target_client = params.target_client logger.info(f'Using a datetime filter on masters {masters} ' f'with params: services={service_ids}, date={date}, ' f'time={time}, client_id={target_client.id}, ' f'client_name={target_client.first_name}') result = set() good_slots = defaultdict(list) for master in masters: logger.info(f'Checking master {master.first_name}') duration = sum([ service.max_duration for service in master.services.filter(pk__in=service_ids) ]) schedule = master.get_schedule(date) if not schedule: continue can_service = time_slot_utils \ .duration_fits_into_slots( duration, schedule.time_slots.all(), time_from=time, time_to=add_time(time, minutes=duration), ignore_taken_slots=params.ignore_taken_slots) logger.info(f'Master {master.first_name} can do all services' f'{service_ids} on {date} = {can_service}') # checking the closest order that goes before `time` if can_service and gmaps_utils.can_reach( schedule, target_client.home_address.location, time): logger.info(f'Selecting master {master.first_name}') result.add(master) good_slots[master.id].append({ 'date': schedule.date.strftime('%Y-%m-%d'), 'time_slots': [datetime.time.strftime(time, '%H:%M')] }) return result, good_slots
def post(self, request, *args, **kwargs): """ Returns possible upsale options with respect to the provided order configuration Input: ``` { 'date': '2017-10-18', 'time': '11:00', 'order_items': [{ 'master_id': 11, 'service_ids': [25] }, { 'master_id': 11, 'service_ids': [16] }] } ``` Response: 200 OK ``` [{ 'master_id': 10, 'service_id': 50 }] ``` """ serializer = RecommendationInputSerializer(data=request.data) serializer.is_valid(raise_exception=True) order_date = serializer.validated_data['date'] order_time = serializer.validated_data['time'] order_items = serializer.validated_data['order_items'] for item in order_items: # expected service execution start time item['time'] = add_time(source_time=order_time, minutes=sum([ service.max_duration for service in Service.objects.filter( pk__in=item['service_ids']) ])) result = master_utils.upsale_search(order_items, order_date) return Response(data=result)
def test_fits_duration(self): time_slots = [ TimeSlot(time=_make_time(10, 30), taken=True), TimeSlot(time=_make_time(11, 00), taken=False), TimeSlot(time=_make_time(11, 30), taken=False), TimeSlot(time=_make_time(12, 00), taken=False), TimeSlot(time=_make_time(12, 30), taken=False), ] # max - 60 - 2+1 slots time_from = datetime.time(hour=11, minute=30) duration = 2 * TimeSlot.DURATION result = time_slot_utils.duration_fits_into_slots( duration, time_slots, time_from=time_from, time_to=add_time(time_from, minutes=duration)) self.assertTrue(result)
def find_replacement_masters(order, order_items, old_master): """ Tries to find replacement masters for the `old_master` in all `order_items` in the `order` :return: True if masters in all `order_items` were successfully replaced """ holder = [] logger.info(f'Looking for a replacement master ' f'for {old_master.first_name}. order_id={order.id}') time = order.time for order_item in order_items: logger.info(f'Checking order_item {order_item.id} ' f'with service {order_item.service.id}') if order_item.locked: raise PermissionDenied(detail='You are not allowed ' 'to cancel a locked order') replacement = _find_replacement_master(order, time, order_item.service.id, old_master) if replacement: logger.info(f'Replacement for order_item={order_item.id} found!' f' New master {replacement.first_name}') holder.append((order, order_item, replacement)) time = time_slot_utils.add_time( time, minutes=order_item.service.max_duration) else: # at least one replacement not found, drop the order logger.info(f'Replacement for order_item={order_item.id} ' f'was not found. Dropping the order {order.id}') order.delete() return False # no breaks, recalculate the balance for order, order_item, replacement in holder: # this money is not yours anymore order_item.master.cancel_order_payment(order, order_item) # it's for the new guy replacement.create_order_payment(order, order_item) order_item.master = replacement order_item.save() return True