Compare commits
	
		
			No commits in common. "58ed948975fdbe561b04d937be3e20d7abd902f2" and "b58efa1fc27a377590b7c93c16859da96f6bc244" have entirely different histories.
		
	
	
		
			58ed948975
			...
			b58efa1fc2
		
	
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -3,5 +3,3 @@ | |||||||
| .setjonpass | .setjonpass | ||||||
| elm-stuff | elm-stuff | ||||||
| static/jon.js | static/jon.js | ||||||
| __pycache__ |  | ||||||
| *.swp |  | ||||||
|  | |||||||
| @ -1,26 +0,0 @@ | |||||||
| 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 |  | ||||||
| @ -1,41 +0,0 @@ | |||||||
| 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 |  | ||||||
| @ -1,59 +0,0 @@ | |||||||
| -- 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; |  | ||||||
| 
 |  | ||||||
| @ -1,3 +0,0 @@ | |||||||
| INSERT INTO garfield.inventory_correction (item_id, delta, correction_comment) |  | ||||||
| VALUES (%(item_id)s, %(correction_delta)s, %(correction_comment)s) |  | ||||||
| 
 |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| 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 |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| 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 |  | ||||||
| @ -1,4 +0,0 @@ | |||||||
| SELECT |  | ||||||
|   * |  | ||||||
| FROM all_inventory_item_overview |  | ||||||
| WHERE item_id = %(item_id)s |  | ||||||
| @ -1,6 +0,0 @@ | |||||||
| SELECT |  | ||||||
|   * |  | ||||||
| FROM all_inventory_item_overview |  | ||||||
| WHERE item_barcode = %(item_barcode)s |  | ||||||
|   AND location = %(location_id)s |  | ||||||
| ORDER BY available DESC, bought DESC |  | ||||||
| @ -1,3 +0,0 @@ | |||||||
| SELECT * |  | ||||||
| FROM garfield.locations |  | ||||||
| WHERE location_id = %(location_id)s |  | ||||||
| @ -1,2 +0,0 @@ | |||||||
| SELECT * |  | ||||||
| FROM garfield.locations |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| 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 |  | ||||||
| @ -1,82 +0,0 @@ | |||||||
| 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) |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| 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 |  | ||||||
|     }) |  | ||||||
| @ -1,12 +0,0 @@ | |||||||
| 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 "❌" |  | ||||||
| @ -1,87 +0,0 @@ | |||||||
| <!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> |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| {% extends "base.html" %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
| <h2>It works!</h2> |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,34 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @ -1,147 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @ -1,14 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @ -1,8 +0,0 @@ | |||||||
| 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,9 +5,13 @@ | |||||||
| 
 | 
 | ||||||
| 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,11 +21,3 @@ 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