Add python

This commit is contained in:
Paul Brinkmeier 2023-05-17 12:43:43 +02:00
parent e31017831a
commit b886d71a0b
17 changed files with 458 additions and 0 deletions

26
py/jon/__init__.py Normal file

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

38
py/jon/db/__init__.py Normal file

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

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

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

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

@ -0,0 +1,3 @@
SELECT *
FROM garfield.locations
WHERE location_id = %(location_id)s

@ -0,0 +1,2 @@
SELECT *
FROM garfield.locations

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

40
py/jon/inventory.py Normal file

@ -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("/<item_id>")
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
})

26
py/jon/location.py Normal file

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

12
py/jon/template_utils.py Normal file

@ -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 ""

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>jon</title>
<style>
h1 {
margin: 0;
}
nav > ul {
padding-left: 0;
}
nav > ul > li {
display: inline-block;
list-style: none;
}
nav > ul > li + li:before {
content: ' · ';
}
.current-page > a {
position: relative;
}
.current-page > a:after {
content: '↓';
font-size: 0.8em;
box-sizing: border-box;
position: absolute;
display: block;
right: 50%;
top: -1em;
width: 1em;
text-align: center;
margin-right: -0.5em;
animation: wiggle 0.8s ease-in-out 0s infinite;
/* animation-direction: alternate; */
}
.number-column {
text-align: right;
}
@keyframes wiggle {
0%, 100% { margin-top: 0; }
50% { margin-top: -0.5em; }
/* 100% { transform: rotate(1turn); } */
}
table {
border-spacing: .5em 0;
}
th {
font-size: .8em;
}
</style>
</head>
<body>
<header>
<h1>jon</h1>
<nav>
<ul>
<li {{ "class=current-page" if request.path == "/" else "" }}><a href="/">Home</a></li>
<li {{ "class=current-page" if request.path.startswith("/inventory") else "" }}><a href="/inventory">Inventar</a></li>
<li {{ "class=current-page" if request.path.startswith("/location") else "" }}>
<a href="/location">
{% if "location" not in session %}
Raum wählen
{% else %}
Raum: {{ session.location.location_name }}
{% endif %}
</a>
</li>
</ul>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h2>It works!</h2>
{% endblock %}

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block content %}
<table>
<tr>
<th>ID</th>
<th>Barcode</th>
<th>Name</th>
<th>Preis (Netto)</th>
<th>Kaufdatum</th>
<th>Gruppe</th>
<th>Inventar</th>
<th title="Korrekturen">Korr.</th>
<th>Eingekauft</th>
<th title="Aktive Snackeinträge">AS</th>
<th>Raum</th>
</tr>
{% for item in items %}
<tr>
<td><a href="/inventory/{{ item.item_id }}">{{ item.item_id }}</a></td>
<td><code>{{ item.item_barcode }}</code></td>
<td>{{ item.name }}</td>
<td class="number-column">{{ format_currency(item.unit_price) }}</td>
<td>{{ format_date(item.bought) }}</td>
<td>{{ item.group_name }}</td>
<td class="number-column">{{ item.units_left }}</td>
<td class="number-column">{{ item.correction_delta }}</td>
<td class="number-column">{{ item.sales_units }}</td>
<td class="number-column">{{ item.active_mappings }}</td>
<td>{{ item.location_name }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block content %}
<h2>Inventareintrag {{ item.item_id }}</h2>
<fieldset>
<legend>Inventareintrag {{ item.item_id }}</legend>
<table>
<tr>
<th style="text-align: left;">ID</th>
<td>{{ item.item_id }}</td>
</tr>
<tr>
<th style="text-align: left;">Barcode</th>
<td><code>{{ item.item_barcode }}</code></td>
</tr>
<tr>
<th style="text-align: left;">Name</th>
<td>{{ item.name }}</td>
</tr>
<tr>
<th style="text-align: left;">Einkaufspreis (Netto)</th>
<td>{{ format_currency(item.unit_price) }}</td>
</tr>
<tr>
<th style="text-align: left;">Kaufdatum</th>
<td>{{ format_date(item.bought) }}</td>
</tr>
<tr>
<th style="text-align: left;">Gruppe</th>
<td>{{ item.group_name }} ({{ item.item_group }})</td>
</tr>
<tr>
<th style="text-align: left;">Inventar</th>
<td>{{ item.units_left }}</td>
</tr>
<tr>
<th style="text-align: left;">Korrekturen</th>
<td>{{ item.correction_delta }}</td>
</tr>
<tr>
<th style="text-align: left;">Eingekauft</th>
<td>{{ item.sales_units }}</td>
</tr>
<tr>
<th style="text-align: left;">Raum</th>
<td>{{ item.location_name }} ({{ item.location }})</td>
</tr>
<tr>
<th style="text-align: left;">Aktiv?</th>
<td>{{ format_bool(item.available) }}</td>
</tr>
</table>
</fieldset>
<fieldset>
<legend>Snackeinträge für {{ item.item_id }}</legend>
<table>
<tr>
<th>ID</th>
<th>Barcode</th>
<th>Name</th>
<th>Verkaufspreis (Brutto)</th>
<th>Eintragedatum</th>
<th>Steuersatz</th>
<th>Raum</th>
<th>Aktiv?</th>
</tr>
{% for snack in snacks %}
<tr>
<td>{{ snack.snack_id }}</td>
<td><code>{{ snack.snack_barcode }}</code></td>
<td>{{ snack.snack_name }}</td>
<td>{{ format_currency(snack.snack_price) }}</td>
<td>{{ format_date(snack.snack_timestamp) }}</td>
<td>{{ snack.description }} ({{ snack.tax_group_id }})</td>
<td>{{ snack.location_name }} ({{ snack.location_id }})</td>
<td>{{ format_bool(snack.active) }}</td>
</tr>
{% endfor %}
</table>
</fieldset>
<fieldset>
<legend>Aktive Inventareinträge mit Barcode <code>{{ item.item_barcode }}</code> in {{ item.location_name }}</legend>
<table>
<tr>
<th>ID</th>
<th>Barcode</th>
<th>Name</th>
<th>Einkaufspreis (Netto)</th>
<th>Kaufdatum</th>
<th>Gruppe</th>
<th>Inventar</th>
<th>Korrekturen</th>
<th>Eingekauft</th>
<th>Raum</th>
</tr>
{% for item in same_barcode_items %}
<tr>
<td><a href="/inventory/{{ item.item_id }}">{{ item.item_id }}</a></td>
<td><code>{{ item.item_barcode }}</code></td>
<td>{{ item.name }}</td>
<td>{{ format_currency(item.unit_price) }}</td>
<td>{{ format_date(item.bought) }}</td>
<td>{{ item.group_name }} ({{ item.item_group }})</td>
<td>{{ item.units_left }}</td>
<td>{{ item.correction_delta }}</td>
<td>{{ item.sales_units }}</td>
<td>{{ item.location_name }} ({{ item.location }})</td>
</tr>
{% endfor %}
</table>
</fieldset>
{% endblock %}

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" action=".">
<select name="location_id">
<option value="" {{ "selected" if "location" not in session else ""}}>-</option>
{% for location in locations %}
<option value="{{ location.location_id }}" {{ "selected" if "location" in session and session.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option>
{% endfor %}
</select>
<button type="submit">Raum wählen</button>
</form>
{% endblock %}

8
py/requirements.txt Normal file

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