Compare commits
	
		
			3 Commits
		
	
	
		
			b58efa1fc2
			...
			bbd51978cb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bbd51978cb | |||
| b886d71a0b | |||
| e31017831a | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -3,3 +3,5 @@ | |||||||
| .setjonpass | .setjonpass | ||||||
| elm-stuff | elm-stuff | ||||||
| static/jon.js | static/jon.js | ||||||
|  | __pycache__ | ||||||
|  | *.swp | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								py/jon/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | ||||||
							
								
								
									
										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; | ||||||
|  | 
 | ||||||
							
								
								
									
										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_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 | ||||||
							
								
								
									
										40
									
								
								py/jon/inventory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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 "❌" | ||||||
							
								
								
									
										87
									
								
								py/jon/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								py/jon/templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | <!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; | ||||||
|  |       } | ||||||
|  |     </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> | ||||||
							
								
								
									
										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_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 %} | ||||||
							
								
								
									
										125
									
								
								py/jon/templates/inventory/read_item.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								py/jon/templates/inventory/read_item.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | |||||||
|  | {% 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>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_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 | ||||||
| @ -5,13 +5,9 @@ | |||||||
| 
 | 
 | ||||||
| module Jon.Main | module Jon.Main | ||||||
|     ( main |     ( main | ||||||
|     , runFunction |  | ||||||
|     , runQuery |  | ||||||
|     , runIns |  | ||||||
|     ) where |     ) where | ||||||
| 
 | 
 | ||||||
| import Control.Exception (bracket) | import Control.Exception (bracket) | ||||||
| import Database.Beam |  | ||||||
| import Database.Beam.Postgres | import Database.Beam.Postgres | ||||||
| import Servant | import Servant | ||||||
| import Servant.Swagger.UI | import Servant.Swagger.UI | ||||||
|  | |||||||
| @ -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