Example #1
0
    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)
Example #2
0
    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
Example #3
0
    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
Example #4
0
    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)
Example #6
0
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