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 | ||||
| elm-stuff | ||||
| 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 { | ||||
|     background-color: lightblue; | ||||
| } | ||||
| 
 | ||||
| @media print { | ||||
|     .noprint { display: none; } | ||||
| 
 | ||||
|     body > div + div { | ||||
|         display: none !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user