Example #1
0
def get_balance_between_stocktakings(admin):
    """
    Returns the balance between two stocktakingcollections.

    :param admin: Is the administrator user, determined by @adminRequired.

    :return:      A dictionary containing all information about the balance
                  between the stocktakings.
    """
    allowed_params = {'start_id': int, 'end_id': int}
    args = check_allowed_parameters(allowed_params)
    start_id = args.get('start_id', None)
    end_id = args.get('end_id', None)

    # Check for all required arguments
    if not all([start_id, end_id]):
        raise exc.InvalidData()

    # Check the ids.
    if end_id <= start_id:
        raise exc.InvalidData()

    # Query the stocktakingcollections.
    start = StocktakingCollection.query.filter_by(id=start_id).first()
    end = StocktakingCollection.query.filter_by(id=end_id).first()

    # Return the balance.
    balance = _get_balance_between_stocktakings(start, end)
    return jsonify(balance), 200
Example #2
0
def parse_timestamp(data: dict, required: bool) -> dict:
    """
    Parses a timestamp in a input dictionary. If there is no timestamp and it's not required, nothing happens.
    Otherwise an exception gets raised. If a timestamp exists, it gets parsed.

    :param data:       The input dictionary
    :param required:   Flag whether the timestamp is required.
    :return:           The parsed input dictionary.
    """
    # If the timestamp is missing but it is required, raise an exception.
    # Otherwise return the (non-modified) input data.
    if 'timestamp' not in data:
        if required:
            raise exc.DataIsMissing()
        else:
            return data

    # Get the timestamp
    timestamp = data.get('timestamp')

    # If the timestamp is not a string, raise an exception
    if not isinstance(timestamp, str):
        raise exc.WrongType()

    # Catch empty string timestamp which is caused by some JS date pickers
    # inputs when they get cleared. If the timestamp is required, raise an exception.
    if timestamp == '':
        if required:
            raise exc.DataIsMissing()
        else:
            del data['timestamp']
            return data
    else:
        try:
            timestamp = dateutil.parser.parse(data['timestamp'])
            assert isinstance(timestamp, datetime.datetime)
            assert timestamp < datetime.datetime.now(datetime.timezone.utc)
            data['timestamp'] = timestamp.replace(microsecond=0)
        except (TypeError, ValueError, AssertionError):
            raise exc.InvalidData()

    return data
Example #3
0
def get_product_pricehistory(admin, product_id):
    """
    Returns the pricehistory of the product with the given id. If only want to
    query a part of the history in a range there are optional request arguments:
    - start_date:          Is the unix timestamp of the start date.
    - end_date:            Is the unix timestamp of the end date.

    :param admin:          Is the administrator user, determined by
                           @adminRequired.
    :param product_id:     Is the product id.

    :raises EntryNotFound: If the product does not exist.
    :raises WrongType:     If the request args are invalid.

    :return:               The pricehistory of the product.
    """

    # Check, whether the product exists.
    product = Product.query.filter(Product.id == product_id).first()
    if not product:
        raise exc.EntryNotFound()

    # Get the (optional) time range parameters
    try:
        start = request.args.get('start_date')
        if start:
            start = int(start)
        end = request.args.get('end_date')
        if end:
            end = int(end)
    except (TypeError, ValueError):
        raise exc.WrongType()

    # Check whether start lies before end date
    if start and end:
        if not start <= end:
            raise exc.InvalidData()

    history = product.get_pricehistory(start, end)

    return jsonify(history), 200
Example #4
0
def update_replenishmentcollection(admin, id):
    """
    Update the replenishmentcollection with the given id.

    :param admin:                Is the administrator user, determined by
                                 @adminRequired.
    :param id:                   Is the replenishmentcollection id.

    :return:                     A message that the update was successful.

    :raises EntryNotFound:       If the replenishmentcollection with this ID
                                 does not exist.
    :raises ForbiddenField:      If a forbidden field is in the request data.
    :raises UnknownField:        If an unknown parameter exists in the request
                                 data.
    :raises InvalidType:         If one or more parameters have an invalid type.
    :raises NothingHasChanged:   If no change occurred after the update.
    :raises CouldNotUpdateEntry: If any other error occurs.
    :raises EntryNotRevocable:   If the replenishmentcollections was revoked by
                                 by replenishment_update, because all
                                 replenishments are revoked, the revoked field
                                 can not be set to true.
    """
    # Check ReplenishmentCollection
    replcoll = (ReplenishmentCollection.query.filter_by(id=id).first())
    if not replcoll:
        raise exc.EntryNotFound()
    # Which replenishments are not revoked?
    repls = replcoll.replenishments.filter_by(revoked=False).all()

    data = json_body()

    if data == {}:
        raise exc.NothingHasChanged()

    updateable = {'revoked': bool, 'comment': str, 'timestamp': int}
    check_forbidden(data, updateable, replcoll)
    check_fields_and_types(data, None, updateable)

    updated_fields = []
    # Handle replenishmentcollection revoke
    if 'revoked' in data:
        if replcoll.revoked == data['revoked']:
            raise exc.NothingHasChanged()
        # Check if the revoke was caused through the replenishment_update and
        # therefor cant be changed
        if not data['revoked'] and not repls:
            raise exc.EntryNotRevocable()
        replcoll.toggle_revoke(revoked=data['revoked'], admin_id=admin.id)
        del data['revoked']
        updated_fields.append('revoked')

    # Handle new timestamp
    if 'timestamp' in data:
        try:
            timestamp = datetime.datetime.fromtimestamp(data['timestamp'])
            assert timestamp <= datetime.datetime.now()
            replcoll.timestamp = timestamp
            updated_fields.append('revoked')
        except (AssertionError, TypeError, ValueError, OSError, OverflowError):
            """
            AssertionError: The timestamp lies in the future.
            TypeError:      Invalid type for conversion.
            ValueError:     Timestamp is out of valid range.
            OSError:        Value exceeds the data type.
            OverflowError:  Timestamp out of range for platform time_t.
            """
            raise exc.InvalidData()
        del data['timestamp']

    # Handle all other fields
    updated_fields = update_fields(data, replcoll, updated_fields)

    # Apply changes
    try:
        db.session.commit()
    except IntegrityError:
        raise exc.CouldNotUpdateEntry()

    return jsonify({
        'message': 'Updated replenishmentcollection.',
        'updated_fields': updated_fields
    }), 201
Example #5
0
def create_stocktakingcollections(admin):
    """
    Insert a new stocktakingcollection.

    :param admin:                Is the administrator user, determined by
                                 @adminRequired.

    :return:                     A message that the creation was successful.

    :raises DataIsMissing:       If not all required data is available.
    :raises ForbiddenField :     If a forbidden field is in the data.
    :raises WrongType:           If one or more data is of the wrong type.
    :raises EntryNotFound:       If the product with with the id of any
                                 replenishment does not exist.
    :raises InvalidAmount:       If amount of any replenishment is less than
                                 or equal to zero.
    :raises CouldNotCreateEntry: If any other error occurs.
    """
    data = json_body()
    required = {'stocktakings': list, 'timestamp': int}
    required_s = {'product_id': int, 'count': int}
    optional_s = {'keep_active': bool}

    # Check all required fields
    check_fields_and_types(data, required)

    stocktakings = data['stocktakings']
    # Check for stocktakings in the collection
    if not stocktakings:
        raise exc.DataIsMissing()

    for stocktaking in stocktakings:
        product_id = stocktaking.get('product_id')
        product = Product.query.filter_by(id=product_id).first()
        if not product:
            raise exc.EntryNotFound()
        if not product.countable:
            raise exc.InvalidData()

    # Get all active product ids
    products = (Product.query.filter(Product.active.is_(True)).filter(
        Product.countable.is_(True)).all())
    active_ids = list(map(lambda p: p.id, products))
    data_product_ids = list(map(lambda d: d['product_id'], stocktakings))

    # Compare function
    def compare(x, y):
        return collections.Counter(x) == collections.Counter(y)

    # We need an entry for all active products. If some data is missing,
    # raise an exception
    if not compare(active_ids, data_product_ids):
        raise exc.DataIsMissing()

    # Check the timestamp
    try:
        timestamp = datetime.datetime.fromtimestamp(data['timestamp'])
        assert timestamp <= datetime.datetime.now()
    except (AssertionError, TypeError, ValueError, OSError, OverflowError):
        # AssertionError: The timestamp is after the current time.
        # TypeError:      Invalid type for conversion.
        # ValueError:     Timestamp is out of valid range.
        # OSError:        Value exceeds the data type.
        # OverflowError:  Timestamp out of range for platform time_t.
        raise exc.InvalidData()
    # Create stocktakingcollection
    collection = StocktakingCollection(admin_id=admin.id, timestamp=timestamp)
    db.session.add(collection)
    db.session.flush()

    # Check for all required data and types
    for stocktaking in stocktakings:

        # Check all required fields
        check_fields_and_types(stocktaking, required_s, optional_s)

        # Get all fields
        product_id = stocktaking.get('product_id')
        count = stocktaking.get('count')
        keep_active = stocktaking.get('keep_active', False)

        # Check amount
        if count < 0:
            raise exc.InvalidAmount()

        # Does the product changes its active state?
        product = Product.query.filter_by(id=product_id).first()
        if count == 0 and keep_active is False:
            product.active = False

    # Create and insert stocktakingcollection
    try:
        for stocktaking in stocktakings:
            s = Stocktaking(collection_id=collection.id,
                            product_id=stocktaking.get('product_id'),
                            count=stocktaking.get('count'))
            db.session.add(s)
        db.session.commit()

    except IntegrityError:
        raise exc.CouldNotCreateEntry()

    return jsonify({'message': 'Created stocktakingcollection.'}), 201
Example #6
0
def _get_product_mean_price_in_time_range(product_id, start, end):
    """
    This function calculates the mean price in a given range of time.

    :param product_id: Is the product id.
    :param start:      Is the start date.
    :param end:        Is the end date.
    :return:           The mean product price in the given time range.
    """

    # Check if start and end dates are date objects.
    if not all([isinstance(d, datetime.datetime) for d in [start, end]]):
        raise exc.InvalidData()

    # If the end date lies before the start date, raise an exception.
    if end <= start:
        raise exc.InvalidData()

    # Check the product id
    if not Product.query.filter_by(id=product_id).first():
        raise exc.EntryNotFound()

    # Get product price at the time of the first stocktaking.
    res1 = (ProductPrice.query.filter(
        ProductPrice.product_id == product_id).filter(
            ProductPrice.timestamp <= start).order_by(
                ProductPrice.timestamp.desc()).first())

    # Get all price changes in the range between the two stocktakings
    res2 = (ProductPrice.query.filter(
        ProductPrice.product_id == product_id).filter(
            and_(ProductPrice.timestamp < end,
                 ProductPrice.timestamp > start)).order_by(
                     ProductPrice.timestamp).all())

    # Get a list of all product price changes
    changes = [res1] + res2

    # If no price could be determined, take the current price.
    if not changes or not changes[0]:
        return Product.query.filter_by(id=product_id).first().price
    # If there are no resulting changes, return the first price.
    elif len(changes) == 1:
        return changes[0].price

    # Iterate over all days in the time range and calculate the mean price
    else:
        # Get the timestamp of the first entry.
        first = changes[0].timestamp

        # Shift the first timestamp to the beginning of the day and the last
        # timestamp to the end of the day.
        first = _shift_date_to_begin_of_day(first)
        start = _shift_date_to_begin_of_day(start)
        end = _shift_date_to_end_of_day(end)

        # Shift the timestamp of all changes to the beginning of the day.
        for item in changes:
            item.timestamp = _shift_date_to_begin_of_day(item.timestamp)

        # Set the current date to the first entry.
        current_date = first
        current_price = changes[0].price
        day_count = 0
        sum_price = 0

        # Iterate over all days and increment the day count and sum all prices.
        while current_date <= end:
            current_date += datetime.timedelta(days=1)
            if current_date <= start:
                continue
            day_count += 1
            sum_price += current_price
            if current_date in list(map(lambda x: x.timestamp, changes)):
                current_price = next(item for item in changes
                                     if item.timestamp == current_date).price

        # Return the mean product price as integer.
        return int(round(sum_price / day_count))