[pricelist] first commit for pricelist plugin

This commit is contained in:
Tim Gröger 2021-02-13 14:13:46 +01:00
parent 29157dbc03
commit a6a1de19de
5 changed files with 450 additions and 0 deletions

View File

@ -0,0 +1,127 @@
"""Pricelist plugin"""
from flask import Blueprint, jsonify, request
from http.client import NO_CONTENT
from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required
from werkzeug.exceptions import BadRequest
from . import models
from . import pricelist_controller, permissions
pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist")
class PriceListPlugin(Plugin):
models = models
def __init__(self, cfg):
super().__init__(blueprint=pricelist_bp, permissions=permissions.permissions)
config = {"discount": 0}
config.update(cfg)
def install(self):
from flaschengeist.database import db
db.create_all()
@pricelist_bp.route("/drink-types", methods=["GET"])
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["GET"])
def get_drink_types(identifier=None):
if identifier:
result = pricelist_controller.get_drink_type(identifier)
else:
result = pricelist_controller.get_drink_types()
return jsonify(result)
@pricelist_bp.route("/drink-types", methods=["POST"])
@login_required(permission=permissions.CREATE_TYPE)
def new_drink_type(current_session):
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.create_drink_type(data["name"])
return jsonify(drink_type)
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TYPE)
def update_drink_type(identifier, current_session):
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.rename_drink_type(data["id"], data["name"])
return jsonify(drink_type)
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TYPE)
def delete_drink_type(identifier, current_session):
pricelist_controller.delete_drink_type(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/tags", methods=["GET"])
@pricelist_bp.route("/tags/<int:identifier>", methods=["GET"])
def get_tags(identifier=None):
if identifier:
result = pricelist_controller.get_tag(identifier)
else:
result = pricelist_controller.get_tags()
return jsonify(result)
@pricelist_bp.route("/tags", methods=["POST"])
@login_required(permission=permissions.CREATE_TAG)
def new_tag(current_session):
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.create_tag(data["name"])
return jsonify(drink_type)
@pricelist_bp.route("/tags/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TAG)
def update_tag(identifier, current_session):
data = request.get_json()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.rename_tag(data["name"])
return jsonify(drink_type)
@pricelist_bp.route("/tags/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TAG)
def delete_tag(identifier, current_session):
pricelist_controller.delete_tag(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/drinks", methods=["GET"])
@pricelist_bp.route("/drinks/<int:identifier>", methods=["GET"])
def get_drinks(identifier=None):
if identifier:
result = pricelist_controller.get_drink(identifier)
else:
result = pricelist_controller.get_drinks()
return jsonify(result)
@pricelist_bp.route("/drinks/search/<string:name>", methods=["GET"])
def search_drinks(name):
return jsonify(pricelist_controller.get_drinks(name))
@pricelist_bp.route("/drinks", methods=["POST"])
@login_required(permission=permissions.CREATE)
def create_drink(current_session):
data = request.get_json()
if not all(item in data for item in ["name", "volume", "cost_price"]) or not all(
item in data for item in ["name", "ingredients"]
):
raise BadRequest("No correct Keys to create drink")
return jsonify(pricelist_controller.create_drink(data))

View File

@ -0,0 +1,90 @@
from flaschengeist.database import db
from flaschengeist.models import ModelSerializeMixin
from typing import Optional
drink_tag_association = db.Table(
"drink_x_tag",
db.Column("drink_id", db.Integer, db.ForeignKey("drink.id")),
db.Column("tag_id", db.Integer, db.ForeignKey("drink_tag.id")),
)
drink_type_association = db.Table(
"drink_x_type",
db.Column("drink_id", db.Integer, db.ForeignKey("drink.id")),
db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")),
)
class Tag(db.Model, ModelSerializeMixin):
"""
Tag
"""
__tablename__ = "drink_tag"
id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
class DrinkType(db.Model, ModelSerializeMixin):
"""
DrinkType
"""
__tablename__ = "drink_type"
id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
class DrinkPrice(db.Model, ModelSerializeMixin):
"""
PriceFromVolume
"""
__tablename__ = "drink_price"
id: int = db.Column("id", db.Integer, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
drink_id = db.Column("drink_id", db.Integer, db.ForeignKey("drink.id"))
drink = db.relationship("Drink", back_populates="prices")
no_auto: bool = db.Column(db.Boolean, default=False)
public: bool = db.Column(db.Boolean, default=True)
description: Optional[str] = db.Column(db.String(30))
round_step: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False, default=0.5)
class Ingredient(db.Model, ModelSerializeMixin):
"""
Drink Build
"""
__tablename__ = "drink_ingredient"
id: int = db.Column("id", db.Integer, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
drink_parent_id: int = db.Column("drink_parent_id", db.Integer, db.ForeignKey("drink.id"))
drink_parent = db.relationship("Drink", foreign_keys=drink_parent_id)
drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id"))
drink_ingredient: "Drink" = db.relationship("Drink", foreign_keys=drink_ingredient_id)
class Drink(db.Model, ModelSerializeMixin):
"""
DrinkPrice
"""
__tablename__ = "drink"
id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(60), nullable=False)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
cost_price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
discount: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
extra_charge: Optional[float] = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), default=0)
prices: [DrinkPrice] = db.relationship(
"DrinkPrice", back_populates="drink", cascade="all,delete,delete-orphan", order_by=[DrinkPrice.volume]
)
ingredients: [Ingredient] = db.relationship(
"Ingredient", back_populates="drink_parent", foreign_keys=Ingredient.drink_parent_id
)
tags: [Optional[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge")
type_id_ = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id"))
type = db.relationship("DrinkType")

View File

@ -0,0 +1,23 @@
CREATE = "drink_create"
"""Can create drinks"""
EDIT = "drink_edit"
"""Can edit drinks"""
DELETE = "drink_delete"
"""Can delete drinks"""
CREATE_TAG = "drink_tag_create"
"""Can create and edit Tags"""
EDIT_TAG = "drink_tag_edit"
DELETE_TAG = "drink_tag_delete"
CREATE_TYPE = "drink_type_create"
EDIT_TYPE = "drink_type_edit"
DELETE_TYPE = "drink_type_delete"
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -0,0 +1,209 @@
from werkzeug.exceptions import BadRequest, NotFound
from sqlalchemy.exc import IntegrityError
from flaschengeist import logger
from flaschengeist.database import db
from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType
from math import ceil
def update():
db.session.commit()
def get_tags():
return Tag.query.all()
def get_tag(identifier):
if isinstance(identifier, int):
retVal = Tag.query.get(identifier)
elif isinstance(identifier, str):
retVal = Tag.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for Tag")
raise BadRequest
if not retVal:
raise NotFound
return retVal
def create_tag(name):
try:
tag = Tag(name=name)
db.session.add(tag)
update()
return tag
except IntegrityError:
raise BadRequest("Name already exists")
def rename_tag(identifier, new_name):
tag = get_tag(identifier)
tag.name = new_name
try:
update()
except IntegrityError:
raise BadRequest("Name already exists")
def delete_tag(identifier):
tag = get_tag(identifier)
db.session.delete(tag)
try:
update()
except IntegrityError:
raise BadRequest("Tag still in use")
def get_drink_types():
return DrinkType.query.all()
def get_drink_type(identifier):
if isinstance(identifier, int):
retVal = DrinkType.query.get(identifier)
elif isinstance(identifier, str):
retVal = DrinkType.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for DrinkType")
raise BadRequest
if not retVal:
raise NotFound
return retVal
def create_drink_type(name):
try:
drinkType = DrinkType(name=name)
db.session.add(drinkType)
update()
return drinkType
except IntegrityError:
raise BadRequest("Name already exists")
def rename_drink_type(identifier, new_name):
drink_type = get_drink_type(identifier)
drink_type.name = new_name
try:
update()
except IntegrityError:
raise BadRequest("Name already exists")
return drink_type
def delete_drink_type(identifier):
drinkType = get_drink_type(identifier)
db.session.delete(drinkType)
try:
update()
except IntegrityError:
raise BadRequest("DrinkType still in use")
def round_price(price, round_step):
return round(ceil(float(price) / round_step) * round_step * 100) / 100
def calc_prices(drink, prices):
retVal = []
if len(drink.ingredients) > 0:
return calc_price_by_ingredients(drink, prices)
allowed_keys = DrinkPrice().serialize().keys()
for price in prices:
values = {key: value for key, value in price.items() if key in allowed_keys}
if values.get("no_auto"):
retVal.append(DrinkPrice(**values))
else:
volume = float(values.pop("volume"))
if "price" in values:
values.pop("price")
_price = float(drink.cost_price) / float(drink.volume) * volume
_price += _price * float(drink.discount)
if drink.extra_charge:
_price += float(drink.extra_charge)
_price = round_price(_price, float(price.get("round_step")))
retVal.append(DrinkPrice(volume=volume, price=_price, **values))
return retVal
def calc_price_by_ingredients(drink, prices):
allowed_keys = DrinkPrice().serialize().keys()
retVal = []
for price in prices:
values = {key: value for key, value in price.items() if key in allowed_keys}
if values.get("no_auto"):
retVal.append(DrinkPrice(**values))
else:
volume = float(values.pop("volume"))
if "price" in values:
values.pop("price")
_price = 0
for ingredient in drink.ingredients:
_price = (
float(ingredient.drink_ingredient.cost_price)
/ float(ingredient.drink_ingredient.volume)
* float(ingredient.volume)
)
_price += _price * float(drink.discount) + float(drink.extra_charge)
_price = round_price(_price, price.get("round_step"))
retVal.append(DrinkPrice(volume=volume, price=_price, **values))
return retVal
def get_drinks(name=None):
if name:
return Drink.query.filter(Drink.name.contains(name)).all()
return Drink.query.all()
def get_drink(identifier):
if isinstance(identifier, int):
retVal = Drink.query.get(identifier)
elif isinstance(identifier, str):
retVal = Drink.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for Drink")
raise BadRequest
if not retVal:
raise NotFound
return retVal
def add_prices(drink, prices):
for price in prices:
drink.prices.append(price)
def add_ingredients(drink, ingredients):
for identifier, volume in ingredients:
ingredient = Ingredient(volume=volume, drink_ingredient=get_drink(identifier))
drink.ingredients.append(ingredient)
def create_drink(data):
allowed_keys = Drink().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
prices = values.pop("prices", [])
ingredients = values.pop("ingredients", [])
if "id" in values:
values.pop("id")
drink = Drink(**values)
add_ingredients(drink, ingredients)
drink.prices = calc_prices(drink, prices)
db.session.add(drink)
update()
return drink
def delete_drink(identifier):
drink = get_drink(identifier)
for price in drink.prices:
db.session.delete(price)
for ingredient in drink.ingredients:
db.session.delete(ingredient)
db.session.delete(drink)
update()

View File

@ -34,6 +34,7 @@ setup(
"balance = flaschengeist.plugins.balance:BalancePlugin",
"schedule = flaschengeist.plugins.schedule:SchedulePlugin",
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
],
},
)