From b886d71a0b41a2a5836914d6e88b8cef3d7754d2 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Wed, 17 May 2023 12:43:43 +0200 Subject: [PATCH] Add python --- py/jon/__init__.py | 26 +++++ py/jon/db/__init__.py | 38 +++++++ py/jon/db/get_inventory_overview.sql | 20 ++++ py/jon/db/get_item_by_id.sql | 18 ++++ py/jon/db/get_items_by_barcode.sql | 9 ++ py/jon/db/get_location_by_id.sql | 3 + py/jon/db/get_locations.sql | 2 + py/jon/db/get_snacks_by_item_id.sql | 8 ++ py/jon/inventory.py | 40 ++++++++ py/jon/location.py | 26 +++++ py/jon/template_utils.py | 12 +++ py/jon/templates/base.html | 78 +++++++++++++++ py/jon/templates/index.html | 5 + py/jon/templates/inventory/index.html | 34 +++++++ py/jon/templates/inventory/read_item.html | 117 ++++++++++++++++++++++ py/jon/templates/location/index.html | 14 +++ py/requirements.txt | 8 ++ 17 files changed, 458 insertions(+) create mode 100644 py/jon/__init__.py create mode 100644 py/jon/db/__init__.py create mode 100644 py/jon/db/get_inventory_overview.sql create mode 100644 py/jon/db/get_item_by_id.sql create mode 100644 py/jon/db/get_items_by_barcode.sql create mode 100644 py/jon/db/get_location_by_id.sql create mode 100644 py/jon/db/get_locations.sql create mode 100644 py/jon/db/get_snacks_by_item_id.sql create mode 100644 py/jon/inventory.py create mode 100644 py/jon/location.py create mode 100644 py/jon/template_utils.py create mode 100644 py/jon/templates/base.html create mode 100644 py/jon/templates/index.html create mode 100644 py/jon/templates/inventory/index.html create mode 100644 py/jon/templates/inventory/read_item.html create mode 100644 py/jon/templates/location/index.html create mode 100644 py/requirements.txt diff --git a/py/jon/__init__.py b/py/jon/__init__.py new file mode 100644 index 0000000..9281ec4 --- /dev/null +++ b/py/jon/__init__.py @@ -0,0 +1,26 @@ +import inspect + +from flask import Flask, render_template + +from . import db, inventory, location, template_utils + + +def create_app(): + app = Flask(__name__) + app.config.from_mapping( + SECRET_KEY="dev" + ) + + db.init_app(app) + + @app.context_processor + def utility_processor(): + return dict(inspect.getmembers(template_utils, inspect.isfunction)) + + app.register_blueprint(location.bp) + app.register_blueprint(inventory.bp) + @app.route("/") + def index(): + return render_template("index.html") + + return app diff --git a/py/jon/db/__init__.py b/py/jon/db/__init__.py new file mode 100644 index 0000000..6a9150b --- /dev/null +++ b/py/jon/db/__init__.py @@ -0,0 +1,38 @@ +import psycopg2 + +from flask import g +from pathlib import Path +from psycopg2.extras import RealDictCursor + + +def get_db(): + if "db" not in g: + # TODO: Make this configurable and use a default that works + # on the pool computers. + g.db = psycopg2.connect("host=localhost dbname=garfield") + + return g.db + + +def close_db(e=None): + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_app(app): + app.teardown_appcontext(close_db) + + +def run_query(query_name, params=None): + db = get_db() + + query = (Path(__file__).parent / Path(query_name)).read_text() + + cursor = db.cursor(cursor_factory=RealDictCursor) + try: + cursor.execute(query, params) + return cursor + except: + db.rollback() + raise diff --git a/py/jon/db/get_inventory_overview.sql b/py/jon/db/get_inventory_overview.sql new file mode 100644 index 0000000..4c50225 --- /dev/null +++ b/py/jon/db/get_inventory_overview.sql @@ -0,0 +1,20 @@ +SELECT + inventory_items.item_id, + inventory_items.item_barcode, + inventory_items.name, + inventory_items.unit_price, + inventory_items.bought, + inventory_item_overview.group_name, + inventory_item_overview.units_left, + inventory_item_overview.correction_delta, + inventory_item_overview.sales_units, + inventory_item_overview.active_mappings, + inventory_item_overview.location_name +FROM garfield.inventory_item_overview +LEFT JOIN garfield.inventory_items USING (item_id) +WHERE + (%(location_id)s IS NULL OR inventory_items.location = %(location_id)s) +ORDER BY + name ASC, + item_barcode DESC, + bought DESC diff --git a/py/jon/db/get_item_by_id.sql b/py/jon/db/get_item_by_id.sql new file mode 100644 index 0000000..fe427f2 --- /dev/null +++ b/py/jon/db/get_item_by_id.sql @@ -0,0 +1,18 @@ +SELECT + inventory_items.item_id, + inventory_items.available, + inventory_items.item_barcode, + inventory_items.name, + inventory_items.unit_price, + inventory_items.bought, + inventory_items.item_group, + inventory_items.location, + inventory_item_overview.group_name, + inventory_item_overview.units_left, + inventory_item_overview.correction_delta, + inventory_item_overview.sales_units, + inventory_item_overview.active_mappings, + inventory_item_overview.location_name +FROM garfield.inventory_item_overview +LEFT JOIN garfield.inventory_items USING (item_id) +WHERE item_id = %(item_id)s diff --git a/py/jon/db/get_items_by_barcode.sql b/py/jon/db/get_items_by_barcode.sql new file mode 100644 index 0000000..415d63d --- /dev/null +++ b/py/jon/db/get_items_by_barcode.sql @@ -0,0 +1,9 @@ +SELECT + * +FROM garfield.inventory_item_overview +LEFT JOIN garfield.inventory_items USING (item_id) +LEFT JOIN garfield.locations ON location = location_id +LEFT JOIN garfield.inventory_item_groups ON item_group = group_id +WHERE item_barcode = %(item_barcode)s + AND location = %(location_id)s +ORDER BY available DESC, bought DESC diff --git a/py/jon/db/get_location_by_id.sql b/py/jon/db/get_location_by_id.sql new file mode 100644 index 0000000..d7677ec --- /dev/null +++ b/py/jon/db/get_location_by_id.sql @@ -0,0 +1,3 @@ +SELECT * +FROM garfield.locations +WHERE location_id = %(location_id)s diff --git a/py/jon/db/get_locations.sql b/py/jon/db/get_locations.sql new file mode 100644 index 0000000..5ef7391 --- /dev/null +++ b/py/jon/db/get_locations.sql @@ -0,0 +1,2 @@ +SELECT * +FROM garfield.locations diff --git a/py/jon/db/get_snacks_by_item_id.sql b/py/jon/db/get_snacks_by_item_id.sql new file mode 100644 index 0000000..c4731cd --- /dev/null +++ b/py/jon/db/get_snacks_by_item_id.sql @@ -0,0 +1,8 @@ +SELECT + * +FROM garfield.inventory_map +LEFT JOIN garfield.snacks USING (snack_id) +LEFT JOIN garfield.snacks_available USING (snack_id) +LEFT JOIN garfield.tax_groups USING (tax_group_id) +LEFT JOIN garfield.locations USING (location_id) +WHERE inventory_id = %(item_id)s diff --git a/py/jon/inventory.py b/py/jon/inventory.py new file mode 100644 index 0000000..42ad120 --- /dev/null +++ b/py/jon/inventory.py @@ -0,0 +1,40 @@ +from flask import Blueprint, render_template, session + +from . import db + + +bp = Blueprint("inventory", __name__, url_prefix="/inventory") + + +@bp.route("/") +def index(): + location = session.get("location", None) + items = db.run_query("get_inventory_overview.sql", { + "location_id": None if location is None else location["location_id"] + }).fetchall() + + return render_template("inventory/index.html", **{ + "items": items + }) + + +@bp.get("/") +def read_item(item_id: int): + item = db.run_query("get_item_by_id.sql", { + "item_id": item_id + }).fetchone() + + snacks = db.run_query("get_snacks_by_item_id.sql", { + "item_id": item_id + }).fetchall() + + same_barcode_items = db.run_query("get_items_by_barcode.sql", { + "item_barcode": item["item_barcode"], + "location_id": item["location"] + }).fetchall() + + return render_template("inventory/read_item.html", **{ + "item": item, + "snacks": snacks, + "same_barcode_items": same_barcode_items + }) diff --git a/py/jon/location.py b/py/jon/location.py new file mode 100644 index 0000000..daf84bd --- /dev/null +++ b/py/jon/location.py @@ -0,0 +1,26 @@ +from flask import Blueprint, render_template, request, session + +from . import db + + +bp = Blueprint("location", __name__, url_prefix="/location") + + +@bp.route("/", methods=["GET", "POST"]) +def index(): + if request.method == "POST": + location_id = request.form.get("location_id", "") + if location_id == "": + session.pop("location", None) + else: + location = db.run_query("get_location_by_id.sql", { + "location_id": location_id} + ).fetchone() + session["location"] = location + + + locations = db.run_query("get_locations.sql").fetchall() + + return render_template("location/index.html", **{ + "locations": locations + }) diff --git a/py/jon/template_utils.py b/py/jon/template_utils.py new file mode 100644 index 0000000..486d49f --- /dev/null +++ b/py/jon/template_utils.py @@ -0,0 +1,12 @@ +def format_currency(x): + # It would be nicer to format this using the German locale + # Too lazy to bother tho. + return f"{x:.02f}€".replace(".", ",") + + +def format_date(d): + return d.strftime("%Y-%m-%d") + + +def format_bool(x): + return "✅" if x else "❌" diff --git a/py/jon/templates/base.html b/py/jon/templates/base.html new file mode 100644 index 0000000..88c4212 --- /dev/null +++ b/py/jon/templates/base.html @@ -0,0 +1,78 @@ + + + + + jon + + + +
+

jon

+ + +
+ +
+ {% block content %}{% endblock %} +
+ + diff --git a/py/jon/templates/index.html b/py/jon/templates/index.html new file mode 100644 index 0000000..b97c9bc --- /dev/null +++ b/py/jon/templates/index.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +

It works!

+{% endblock %} diff --git a/py/jon/templates/inventory/index.html b/py/jon/templates/inventory/index.html new file mode 100644 index 0000000..a790bc0 --- /dev/null +++ b/py/jon/templates/inventory/index.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} + + + + + + + + + + + + + + +{% for item in items %} + + + + + + + + + + + + + +{% endfor %} +
IDBarcodeNamePreis (Netto)KaufdatumGruppeInventarKorr.EingekauftASRaum
{{ item.item_id }}{{ item.item_barcode }}{{ item.name }}{{ format_currency(item.unit_price) }}{{ format_date(item.bought) }}{{ item.group_name }}{{ item.units_left }}{{ item.correction_delta }}{{ item.sales_units }}{{ item.active_mappings }}{{ item.location_name }}
+{% endblock %} diff --git a/py/jon/templates/inventory/read_item.html b/py/jon/templates/inventory/read_item.html new file mode 100644 index 0000000..5eadaa5 --- /dev/null +++ b/py/jon/templates/inventory/read_item.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} + +{% block content %} +

Inventareintrag {{ item.item_id }}

+ +
+ Inventareintrag {{ item.item_id }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ item.item_id }}
Barcode{{ item.item_barcode }}
Name{{ item.name }}
Einkaufspreis (Netto){{ format_currency(item.unit_price) }}
Kaufdatum{{ format_date(item.bought) }}
Gruppe{{ item.group_name }} ({{ item.item_group }})
Inventar{{ item.units_left }}
Korrekturen{{ item.correction_delta }}
Eingekauft{{ item.sales_units }}
Raum{{ item.location_name }} ({{ item.location }})
Aktiv?{{ format_bool(item.available) }}
+
+ +
+ Snackeinträge für {{ item.item_id }} + + + + + + + + + + + + + {% for snack in snacks %} + + + + + + + + + + + {% endfor %} +
IDBarcodeNameVerkaufspreis (Brutto)EintragedatumSteuersatzRaumAktiv?
{{ snack.snack_id }}{{ snack.snack_barcode }}{{ snack.snack_name }}{{ format_currency(snack.snack_price) }}{{ format_date(snack.snack_timestamp) }}{{ snack.description }} ({{ snack.tax_group_id }}){{ snack.location_name }} ({{ snack.location_id }}){{ format_bool(snack.active) }}
+
+ +
+ Aktive Inventareinträge mit Barcode {{ item.item_barcode }} in {{ item.location_name }} + + + + + + + + + + + + + + + {% for item in same_barcode_items %} + + + + + + + + + + + + + {% endfor %} +
IDBarcodeNameEinkaufspreis (Netto)KaufdatumGruppeInventarKorrekturenEingekauftRaum
{{ item.item_id }}{{ item.item_barcode }}{{ item.name }}{{ format_currency(item.unit_price) }}{{ format_date(item.bought) }}{{ item.group_name }} ({{ item.item_group }}){{ item.units_left }}{{ item.correction_delta }}{{ item.sales_units }}{{ item.location_name }} ({{ item.location }})
+
+{% endblock %} diff --git a/py/jon/templates/location/index.html b/py/jon/templates/location/index.html new file mode 100644 index 0000000..c66f8d2 --- /dev/null +++ b/py/jon/templates/location/index.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +
+ + + +
+{% endblock %} diff --git a/py/requirements.txt b/py/requirements.txt new file mode 100644 index 0000000..555baae --- /dev/null +++ b/py/requirements.txt @@ -0,0 +1,8 @@ +blinker==1.6.2 +click==8.1.3 +Flask==2.3.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.2 +psycopg2-binary==2.9.6 +Werkzeug==2.3.4