[pricelist] first commit for pricelist plugin
This commit is contained in:
parent
29157dbc03
commit
a6a1de19de
|
@ -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))
|
|
@ -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")
|
|
@ -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("_")]
|
|
@ -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()
|
1
setup.py
1
setup.py
|
@ -34,6 +34,7 @@ setup(
|
||||||
"balance = flaschengeist.plugins.balance:BalancePlugin",
|
"balance = flaschengeist.plugins.balance:BalancePlugin",
|
||||||
"schedule = flaschengeist.plugins.schedule:SchedulePlugin",
|
"schedule = flaschengeist.plugins.schedule:SchedulePlugin",
|
||||||
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
|
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
|
||||||
|
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue