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
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
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
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
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
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))