Compare commits
5 Commits
e31017831a
...
9211d1f9b4
Author | SHA1 | Date | |
---|---|---|---|
9211d1f9b4 | |||
c44f11a686 | |||
58ed948975 | |||
bbd51978cb | |||
b886d71a0b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@
|
|||||||
.setjonpass
|
.setjonpass
|
||||||
elm-stuff
|
elm-stuff
|
||||||
static/jon.js
|
static/jon.js
|
||||||
|
__pycache__
|
||||||
|
*.swp
|
||||||
|
1
py/TODO.md
Normal file
1
py/TODO.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
- [ ] Fix date handling in entry
|
33
py/jon/__init__.py
Normal file
33
py/jon/__init__.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
db,
|
||||||
|
entry,
|
||||||
|
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.register_blueprint(entry.bp)
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
return app
|
41
py/jon/db/__init__.py
Normal file
41
py/jon/db/__init__.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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")
|
||||||
|
run_query_on(g.db, "add_views.sql", None)
|
||||||
|
|
||||||
|
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):
|
||||||
|
return run_query_on(get_db(), query_name, params)
|
||||||
|
|
||||||
|
|
||||||
|
def run_query_on(db, query_name, params):
|
||||||
|
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
|
59
py/jon/db/add_views.sql
Normal file
59
py/jon/db/add_views.sql
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
-- Modified version of garfield.inventory_item_overview.
|
||||||
|
|
||||||
|
CREATE TEMPORARY VIEW all_inventory_item_overview AS
|
||||||
|
SELECT
|
||||||
|
item_id,
|
||||||
|
inventory_items.item_barcode,
|
||||||
|
inventory_items.bought,
|
||||||
|
inventory_items.name,
|
||||||
|
inventory_items.sales_units,
|
||||||
|
inventory_items.unit_price,
|
||||||
|
inventory_items.available,
|
||||||
|
inventory_items.item_group,
|
||||||
|
inventory_items.location,
|
||||||
|
inventory_item_groups.group_name,
|
||||||
|
COALESCE(b.sales::numeric, 0::numeric) - COALESCE(cancel.count::numeric, 0::numeric) AS sales,
|
||||||
|
inventory_items.sales_units::numeric - COALESCE(b.sales, 0::bigint)::numeric + COALESCE(c.delta, 0::numeric) + COALESCE(cancel.count::numeric, 0::numeric) AS units_left,
|
||||||
|
COALESCE(c.delta, 0::numeric) AS correction_delta,
|
||||||
|
COALESCE(m.mappings::numeric, 0::numeric) AS active_mappings,
|
||||||
|
m.mappings_array AS active_mappings_array,
|
||||||
|
locations.location_name
|
||||||
|
FROM garfield.inventory_items
|
||||||
|
JOIN garfield.locations ON inventory_items.location = locations.location_id
|
||||||
|
LEFT JOIN garfield.inventory_item_groups ON inventory_item_groups.group_id = inventory_items.item_group
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
snack_sales_log.inventory_line AS item_id,
|
||||||
|
count(*) AS sales
|
||||||
|
FROM garfield.snack_sales_log
|
||||||
|
WHERE snack_sales_log.inventory_line IS NOT NULL
|
||||||
|
AND snack_sales_log.type_id::text = 'SNACK_BUY'::text
|
||||||
|
GROUP BY snack_sales_log.inventory_line
|
||||||
|
) b USING (item_id)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
snack_sales_log.inventory_line AS item_id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM garfield.snack_sales_log
|
||||||
|
WHERE snack_sales_log.inventory_line IS NOT NULL
|
||||||
|
AND snack_sales_log.type_id::text = 'SNACK_CANCEL'::text
|
||||||
|
GROUP BY snack_sales_log.inventory_line
|
||||||
|
) cancel USING (item_id)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
inventory_correction.item_id,
|
||||||
|
sum(inventory_correction.delta) AS delta
|
||||||
|
FROM garfield.inventory_correction
|
||||||
|
GROUP BY inventory_correction.item_id
|
||||||
|
) c USING (item_id)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
count(inventory_map.snack_id) AS mappings,
|
||||||
|
array_agg(inventory_map.snack_id) AS mappings_array,
|
||||||
|
inventory_map.inventory_id AS item_id
|
||||||
|
FROM garfield.inventory_map
|
||||||
|
JOIN garfield.snacks_available ON snacks_available.snack_available AND snacks_available.snack_id = inventory_map.snack_id
|
||||||
|
GROUP BY inventory_map.inventory_id
|
||||||
|
) m USING (item_id)
|
||||||
|
ORDER BY inventory_items.name;
|
||||||
|
|
3
py/jon/db/create_correction.sql
Normal file
3
py/jon/db/create_correction.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO garfield.inventory_correction (item_id, delta, correction_comment)
|
||||||
|
VALUES (%(item_id)s, %(correction_delta)s, %(correction_comment)s)
|
||||||
|
|
9
py/jon/db/deactivate_item.sql
Normal file
9
py/jon/db/deactivate_item.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
UPDATE garfield.inventory_items
|
||||||
|
SET available = FALSE
|
||||||
|
WHERE item_id = %(item_id)s;
|
||||||
|
|
||||||
|
-- Call garfield.snack_delete for every snack entry associated with this item.
|
||||||
|
SELECT snack_id
|
||||||
|
FROM garfield.inventory_map,
|
||||||
|
LATERAL garfield.snack_delete(snack_id::integer)
|
||||||
|
WHERE inventory_id = %(item_id)s
|
6
py/jon/db/get_groups.sql
Normal file
6
py/jon/db/get_groups.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SELECT
|
||||||
|
group_id,
|
||||||
|
group_name
|
||||||
|
FROM garfield.inventory_item_groups
|
||||||
|
ORDER BY
|
||||||
|
group_name ASC
|
9
py/jon/db/get_inventory_overview.sql
Normal file
9
py/jon/db/get_inventory_overview.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM all_inventory_item_overview
|
||||||
|
WHERE (%(location_id)s IS NULL OR location = %(location_id)s)
|
||||||
|
AND available
|
||||||
|
ORDER BY
|
||||||
|
name ASC,
|
||||||
|
item_barcode DESC,
|
||||||
|
bought DESC
|
4
py/jon/db/get_item_by_id.sql
Normal file
4
py/jon/db/get_item_by_id.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM all_inventory_item_overview
|
||||||
|
WHERE item_id = %(item_id)s
|
6
py/jon/db/get_items_by_barcode.sql
Normal file
6
py/jon/db/get_items_by_barcode.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM all_inventory_item_overview
|
||||||
|
WHERE item_barcode = %(item_barcode)s
|
||||||
|
AND location = %(location_id)s
|
||||||
|
ORDER BY available DESC, bought DESC
|
3
py/jon/db/get_location_by_id.sql
Normal file
3
py/jon/db/get_location_by_id.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
SELECT *
|
||||||
|
FROM garfield.locations
|
||||||
|
WHERE location_id = %(location_id)s
|
2
py/jon/db/get_locations.sql
Normal file
2
py/jon/db/get_locations.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
SELECT *
|
||||||
|
FROM garfield.locations
|
11
py/jon/db/get_snacks_by_barcode.sql
Normal file
11
py/jon/db/get_snacks_by_barcode.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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 snack_barcode = %(snack_barcode)s
|
||||||
|
ORDER BY
|
||||||
|
snack_available DESC,
|
||||||
|
snack_timestamp DESC
|
11
py/jon/db/get_snacks_by_item_id.sql
Normal file
11
py/jon/db/get_snacks_by_item_id.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
|
ORDER BY
|
||||||
|
snack_available DESC,
|
||||||
|
snack_timestamp DESC
|
59
py/jon/entry.py
Normal file
59
py/jon/entry.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import datetime
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
|
|
||||||
|
from flask import Blueprint, redirect, render_template, request, session
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint("entry", __name__, url_prefix="/entry")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/")
|
||||||
|
def index():
|
||||||
|
return render_template("entry/index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/edit-item-data", methods=["GET", "POST"])
|
||||||
|
def edit_item_data():
|
||||||
|
if "entry" not in session:
|
||||||
|
session["entry"] = dict()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
session["entry"] = {
|
||||||
|
"item_bought": datetime.datetime.strptime(request.form.get("item_bought"), "%Y-%m-%d"),
|
||||||
|
"item_barcode": request.form.get("item_barcode"),
|
||||||
|
"item_name": request.form.get("item_name"),
|
||||||
|
"item_group_id": int(request.form.get("item_group")),
|
||||||
|
"item_net_unit_price": float(request.form.get("item_net_unit_price")),
|
||||||
|
"item_tax_group_id": int(request.form.get("item_tax_group")),
|
||||||
|
"item_amount": int(request.form.get("item_amount")),
|
||||||
|
"item_location_id": int(request.form.get("item_location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect("/entry/select-snack-entry")
|
||||||
|
|
||||||
|
groups = db.run_query("get_groups.sql").fetchall()
|
||||||
|
locations = db.run_query("get_locations.sql").fetchall()
|
||||||
|
|
||||||
|
return render_template("entry/edit-item-data.html", **{
|
||||||
|
"groups": groups,
|
||||||
|
"locations": locations,
|
||||||
|
"entry": session["entry"]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/select-snack-entry", methods=["GET", "POST"])
|
||||||
|
def edit_snack_data():
|
||||||
|
if "entry" not in session:
|
||||||
|
return redirect("/entry/edit-item-data")
|
||||||
|
|
||||||
|
snacks = db.run_query("get_snacks_by_barcode.sql", {
|
||||||
|
"snack_barcode": session["entry"]["item_barcode"]
|
||||||
|
}).fetchall()
|
||||||
|
|
||||||
|
return render_template("entry/select-snack-entry.html", **{
|
||||||
|
"entry": session["entry"],
|
||||||
|
"snacks": snacks
|
||||||
|
})
|
82
py/jon/inventory.py
Normal file
82
py/jon/inventory.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from flask import Blueprint, redirect, render_template, request, 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/<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
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/item/<item_id>/deactivate")
|
||||||
|
def deactivate_item(item_id: int):
|
||||||
|
item = db.run_query("get_item_by_id.sql", {
|
||||||
|
"item_id": item_id
|
||||||
|
}).fetchone()
|
||||||
|
|
||||||
|
if item["units_left"] != 0:
|
||||||
|
return "Only items without stock can be deactivated", 400
|
||||||
|
|
||||||
|
db.run_query("deactivate_item.sql", {
|
||||||
|
"item_id": item_id
|
||||||
|
})
|
||||||
|
db.get_db().commit()
|
||||||
|
|
||||||
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/correction")
|
||||||
|
def create_correction():
|
||||||
|
try:
|
||||||
|
item_id = int(request.form.get("item_id"))
|
||||||
|
correction_delta = int(request.form.get("correction_delta"))
|
||||||
|
correction_comment = request.form.get("correction_comment")
|
||||||
|
except:
|
||||||
|
return "Incomplete or mistyped form", 400
|
||||||
|
|
||||||
|
if correction_delta == 0:
|
||||||
|
return "Correction delta may not be 0", 400
|
||||||
|
|
||||||
|
if correction_comment == "":
|
||||||
|
return "Correction comment may not be empty", 400
|
||||||
|
|
||||||
|
db.run_query("create_correction.sql", {
|
||||||
|
"item_id": item_id,
|
||||||
|
"correction_delta": correction_delta,
|
||||||
|
"correction_comment": correction_comment
|
||||||
|
})
|
||||||
|
db.get_db().commit()
|
||||||
|
|
||||||
|
return redirect(request.referrer)
|
26
py/jon/location.py
Normal file
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
|
||||||
|
})
|
30
py/jon/template_utils.py
Normal file
30
py/jon/template_utils.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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 "❌"
|
||||||
|
|
||||||
|
|
||||||
|
def now():
|
||||||
|
return datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def get_garfield_price(net_unit_price, tax_group_id):
|
||||||
|
if tax_group_id == 1:
|
||||||
|
tax_factor = 1.19
|
||||||
|
elif tax_group_id == 2:
|
||||||
|
tax_factor = 1.07
|
||||||
|
else:
|
||||||
|
raise Error("Unknown tax group ID")
|
||||||
|
|
||||||
|
return net_unit_price * tax_factor + 0.01
|
100
py/jon/templates/base.html
Normal file
100
py/jon/templates/base.html
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>jon</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
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; */
|
||||||
|
}
|
||||||
|
.--align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.--align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.--centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-input > label {
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
.form-input > input:not([type=radio]),
|
||||||
|
.form-input > select {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</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("/entry") else "" }}><a href="/entry">Eintragen</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>
|
56
py/jon/templates/entry/edit-item-data.html
Normal file
56
py/jon/templates/entry/edit-item-data.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<pre>{{ entry }}</pre>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Neuer Inventareintrag</legend>
|
||||||
|
|
||||||
|
<form method="POST" action="/entry/edit-item-data">
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_bought">Kaufdatum</label>
|
||||||
|
<input name="item_bought" id="item_bought" type="date" value="{{ (entry.item_bought or now()).strftime('%Y-%m-%d') }}">
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_barcode">Barcode</label>
|
||||||
|
<input name="item_barcode" id="item_barcode" type="text" value="{{ entry.item_barcode }}" placeholder="Barcode">
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_name">Artikel</label>
|
||||||
|
<input name="item_name" id="item_name" type="text" value="{{ entry.item_name }}" placeholder="Artikel">
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_group">Gruppe</label>
|
||||||
|
<select name="item_group" id="item_group">
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.group_id }}"{% if entry.item_group_id == group.group_id %} selected{% endif %}>{{ group.group_name }} ({{ group.group_id }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_net_unit_price">Stückpreis (Netto) in €</label>
|
||||||
|
<input name="item_net_unit_price" id="item_net_unit_price" type="number" step="0.01" value="{{ entry.item_net_unit_price }}" placeholder="Stückpreis (Netto) in €">
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<input name="item_tax_group" id="item_tax_group_1" type="radio" value="1"{% if entry.item_tax_group_id == 1 %} selected{% endif %}>
|
||||||
|
<label for="item_tax_group_1">Volle Umsatzsteuer (19%)</label>
|
||||||
|
|
||||||
|
<input name="item_tax_group" id="item_tax_group_2" type="radio" value="2"{% if entry.item_tax_group_id == 2 %} selected{% endif %}>
|
||||||
|
<label for="item_tax_group_2">Ermäßigte Umsatzsteuer (7%)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_amount">Anzahl</label>
|
||||||
|
<input name="item_amount" id="item_amount" type="number" value="{{ entry.item_amount }}" placeholder="Anzahl">
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
|
<label for="item_group">Raum</label>
|
||||||
|
<select name="item_location" id="item_location">
|
||||||
|
{% for location in locations %}
|
||||||
|
<option value="{{ location.location_id }}"{% if entry.item_location_id == location.location_id or ("item_location" not in entry and (session.location.location_id == location.location_id)) %} selected{% endif %}>{{ location.location_name }} ({{ location.location_id }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button>Weiter zu den Snackeinträgen</button>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
{% endblock %}
|
5
py/jon/templates/entry/index.html
Normal file
5
py/jon/templates/entry/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<a href="/entry/edit-item-data">Neuer Eintrag</a>
|
||||||
|
{% endblock %}
|
83
py/jon/templates/entry/select-snack-entry.html
Normal file
83
py/jon/templates/entry/select-snack-entry.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<pre>{{ entry }}</pre>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Neuer Inventareintrag</legend>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">ID</th>
|
||||||
|
<td>{{ entry.item_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Barcode</th>
|
||||||
|
<td><code>{{ entry.item_barcode }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Name</th>
|
||||||
|
<td>{{ entry.item_name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Einkaufspreis (Netto)</th>
|
||||||
|
<td>{{ format_currency(entry.item_net_unit_price) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Empfohlener Garfield-Verkaufspreis</th>
|
||||||
|
<td>{{ format_currency(get_garfield_price(entry.item_net_unit_price, entry.item_tax_group_id)) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Kaufdatum</th>
|
||||||
|
<td>{{ format_date(entry.item_bought) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Gruppe</th>
|
||||||
|
<td>{{ entry.item_group_name }} ({{ entry.item_group_id }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Anzahl</th>
|
||||||
|
<td>{{ entry.item_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Raum</th>
|
||||||
|
<td>{{ entry.item_location_name }} ({{ entry.item_location_id }})</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Snackeinträge mit Barcode <code>{{ entry.item_barcode }}</code></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>
|
||||||
|
<th>Aktionen</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 class="--align-right">{{ 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.snack_available) }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="/entry/select-snack-entry">
|
||||||
|
<input type="hidden" name="snack_id" value="{{ snack.snack_id }}">
|
||||||
|
<button>Snackeintrag übernehmen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
{% endblock %}
|
5
py/jon/templates/index.html
Normal file
5
py/jon/templates/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>It works!</h2>
|
||||||
|
{% endblock %}
|
34
py/jon/templates/inventory/index.html
Normal file
34
py/jon/templates/inventory/index.html
Normal file
@ -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>Eingekauft</th>
|
||||||
|
<th title="Korrekturen">Korr.</th>
|
||||||
|
<th>Inventar</th>
|
||||||
|
<th title="Anzahl aktiver Snackeinträge">#AS</th>
|
||||||
|
<th>Raum</th>
|
||||||
|
</tr>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/inventory/item/{{ item.item_id }}">{{ item.item_id }}</a></td>
|
||||||
|
<td><code>{{ item.item_barcode }}</code></td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td class="--align-right">{{ format_currency(item.unit_price) }}</td>
|
||||||
|
<td>{{ format_date(item.bought) }}</td>
|
||||||
|
<td>{{ item.group_name }} ({{ item.item_group }})</td>
|
||||||
|
<td class="--align-right">{{ item.sales_units }}</td>
|
||||||
|
<td class="--align-right">{% if item.correction_delta > 0 %}+{% endif %}{{ item.correction_delta }}</td>
|
||||||
|
<td class="--align-right">{{ item.units_left }}</td>
|
||||||
|
<td class="--align-right">{{ item.active_mappings }}</td>
|
||||||
|
<td>{{ item.location_name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
147
py/jon/templates/inventory/read_item.html
Normal file
147
py/jon/templates/inventory/read_item.html
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Inventareintrag {{ item.item_id }}</h2>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Inventareintrag {{ item.item_id }}</legend>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">ID</th>
|
||||||
|
<td>{{ item.item_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Barcode</th>
|
||||||
|
<td><code>{{ item.item_barcode }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Name</th>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Einkaufspreis (Netto)</th>
|
||||||
|
<td>{{ format_currency(item.unit_price) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Kaufdatum</th>
|
||||||
|
<td>{{ format_date(item.bought) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Gruppe</th>
|
||||||
|
<td>{{ item.group_name }} ({{ item.item_group }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Inventar</th>
|
||||||
|
<td>{{ item.units_left }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Korrekturen</th>
|
||||||
|
<td>{{ item.correction_delta }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Eingekauft</th>
|
||||||
|
<td>{{ item.sales_units }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Raum</th>
|
||||||
|
<td>{{ item.location_name }} ({{ item.location }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="--align-left">Aktiv?</th>
|
||||||
|
<td>{{ format_bool(item.available) }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Aktionen</legend>
|
||||||
|
|
||||||
|
<form method="POST" action="/inventory/correction">
|
||||||
|
<input name="item_id" type="hidden" value="{{ item.item_id }}">
|
||||||
|
<input name="correction_comment" type="hidden" value="Verlust">
|
||||||
|
<input name="correction_delta" type="hidden" value="{{ -item.units_left }}">
|
||||||
|
<button{% if item.units_left == 0 %} disabled{% endif %}>Inventar zu 0 korrigieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="/inventory/correction">
|
||||||
|
<input name="item_id" type="hidden" value="{{ item.item_id }}">
|
||||||
|
<input name="correction_comment" type="text" value="" placeholder="Kommentar">
|
||||||
|
<input name="correction_delta" type="number" value="" placeholder="Delta">
|
||||||
|
<button>Inventar korrigieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="/inventory/item/{{ item.item_id }}/deactivate">
|
||||||
|
<button{% if item.units_left != 0 %} disabled{% endif %}>Inventar- und Snackeintrag deaktivieren</button>
|
||||||
|
</form>
|
||||||
|
</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 class="--align-right">{{ 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.snack_available) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>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>Eingekauft</th>
|
||||||
|
<th>Korrekturen</th>
|
||||||
|
<th>Inventar</th>
|
||||||
|
<th title="Aktive Snackeinträge">AS</th>
|
||||||
|
<th>Aktiv?</th>
|
||||||
|
</tr>
|
||||||
|
{% for item in same_barcode_items %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/inventory/item/{{ item.item_id }}">{{ item.item_id }}</a></td>
|
||||||
|
<td><code>{{ item.item_barcode }}</code></td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td class="--align-right">{{ format_currency(item.unit_price) }}</td>
|
||||||
|
<td>{{ format_date(item.bought) }}</td>
|
||||||
|
<td>{{ item.group_name }} ({{ item.item_group }})</td>
|
||||||
|
<td class="--align-right">{{ item.sales_units }}</td>
|
||||||
|
<td class="--align-right">{% if item.correction_delta > 0 %}+{% endif %}{{ item.correction_delta }}</td>
|
||||||
|
<td class="--align-right">{{ item.units_left }}</td>
|
||||||
|
<td class="--centered">
|
||||||
|
{% if item.active_mappings != 0 %}
|
||||||
|
{{ item.active_mappings_array | join(", ") }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="--centered">{{ format_bool(item.available) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
{% endblock %}
|
14
py/jon/templates/location/index.html
Normal file
14
py/jon/templates/location/index.html
Normal file
@ -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
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
|
@ -21,3 +21,11 @@ th, td {
|
|||||||
tr:not(:first-child):hover, tbody tr:hover {
|
tr:not(:first-child):hover, tbody tr:hover {
|
||||||
background-color: lightblue;
|
background-color: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.noprint { display: none; }
|
||||||
|
|
||||||
|
body > div + div {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user