Add dynamic inventory type creation

This commit is contained in:
2026-08-29 07:07:23 +03:00
parent 100c0797a4
commit ade6a0931a
6 changed files with 135 additions and 23 deletions
+98 -8
View File
@@ -1,4 +1,5 @@
import os
import json
import shutil
import sqlite3
import tempfile
@@ -22,6 +23,20 @@ ROLE_READ_ONLY = "read_only"
ROLE_READ_WRITE = "read_write"
ROLE_ADMIN = "admin"
VALID_ROLES = {ROLE_READ_ONLY, ROLE_READ_WRITE, ROLE_ADMIN}
CONFIGURABLE_FIELDS = {
"brand", "model", "serial_number", "asset_tag", "location", "destination",
"checkout_date", "purchase_date", "notes", "cpu", "ram", "gpu", "storage", "media_format",
}
DEFAULT_TYPES = {
"Computer": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "cpu", "ram", "gpu", "storage", "notes"],
"Audio": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "media_format", "notes"],
"Graphics": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "gpu", "storage", "notes"],
"Long play disk": ["brand", "model", "asset_tag", "location", "purchase_date", "media_format", "notes"],
"CD": ["brand", "model", "asset_tag", "location", "purchase_date", "media_format", "notes"],
"Display": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
"Peripheral": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
"Other": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
}
def get_db():
@@ -79,6 +94,10 @@ def initialize_db():
can_data_tools INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS inventory_types (
name TEXT PRIMARY KEY COLLATE NOCASE,
fields TEXT NOT NULL
);
"""
)
columns = {row[1] for row in connection.execute("PRAGMA table_info(inventory_items)")}
@@ -94,6 +113,11 @@ def initialize_db():
except sqlite3.OperationalError as error:
if "duplicate column name" not in str(error):
raise
for name, fields in DEFAULT_TYPES.items():
connection.execute(
"INSERT OR IGNORE INTO inventory_types (name, fields) VALUES (?, ?)",
(name, json.dumps(fields)),
)
connection.execute(
"""INSERT INTO inventory_images (item_id, filename, original_name, created_at)
SELECT id, image_filename, image_filename, COALESCE(updated_at, datetime('now'))
@@ -240,17 +264,28 @@ def item_payload(data):
"storage": clean(data.get("storage")),
"media_format": clean(data.get("media_format")),
}
if payload["item_type"] != "Computer":
payload["cpu"] = ""
payload["ram"] = ""
if payload["item_type"] not in {"Computer", "Graphics"}:
payload["gpu"] = ""
payload["storage"] = ""
if payload["item_type"] not in {"CD", "Long play disk", "Audio"}:
payload["media_format"] = ""
return payload
def type_payload(data):
name = clean(data.get("name"))
fields = data.get("fields", [])
if not isinstance(fields, list):
raise ValueError("Choose the fields to show for this inventory type.")
fields = [field for field in fields if field in CONFIGURABLE_FIELDS]
if not name or len(name) > 80:
raise ValueError("A type name of up to 80 characters is required.")
return name, fields
def serialize_type(row):
try:
fields = json.loads(row["fields"])
except json.JSONDecodeError:
fields = []
return {"name": row["name"], "fields": [field for field in fields if field in CONFIGURABLE_FIELDS]}
def serialize(row, connection):
item = dict(row)
item["images"] = [
@@ -343,6 +378,7 @@ def restore_backup(backup):
os.replace(staged_uploads_path, UPLOAD_FOLDER)
else:
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
initialize_db()
@app.route("/")
@@ -387,6 +423,60 @@ def list_items():
return jsonify([serialize(row, connection) for row in rows])
@app.get("/api/types")
@login_required
def list_types():
with get_db() as connection:
rows = connection.execute("SELECT name, fields FROM inventory_types ORDER BY name COLLATE NOCASE").fetchall()
return jsonify([serialize_type(row) for row in rows])
@app.post("/api/types")
@permission_required("admin")
def create_type():
try:
name, fields = type_payload(request.get_json(silent=True) or {})
except ValueError as error:
return jsonify({"error": str(error)}), 400
try:
with get_db() as connection:
connection.execute("INSERT INTO inventory_types (name, fields) VALUES (?, ?)", (name, json.dumps(fields)))
return jsonify({"name": name, "fields": fields}), 201
except sqlite3.IntegrityError:
return jsonify({"error": "That inventory type already exists."}), 409
@app.put("/api/types/<path:type_name>")
@permission_required("admin")
def update_type(type_name):
try:
name, fields = type_payload(request.get_json(silent=True) or {})
except ValueError as error:
return jsonify({"error": str(error)}), 400
with get_db() as connection:
if name.casefold() != type_name.casefold() and connection.execute("SELECT 1 FROM inventory_types WHERE name = ? COLLATE NOCASE", (name,)).fetchone():
return jsonify({"error": "That inventory type already exists."}), 409
result = connection.execute("UPDATE inventory_types SET name = ?, fields = ? WHERE name = ? COLLATE NOCASE", (name, json.dumps(fields), type_name))
if result.rowcount:
connection.execute("UPDATE inventory_items SET item_type = ? WHERE item_type = ? COLLATE NOCASE", (name, type_name))
if result.rowcount == 0:
return jsonify({"error": "Inventory type not found."}), 404
return jsonify({"name": name, "fields": fields})
@app.delete("/api/types/<path:type_name>")
@permission_required("admin")
def delete_type(type_name):
with get_db() as connection:
count = connection.execute("SELECT COUNT(*) FROM inventory_items WHERE item_type = ? COLLATE NOCASE", (type_name,)).fetchone()[0]
if count:
return jsonify({"error": "This type is used by existing inventory and cannot be removed."}), 400
result = connection.execute("DELETE FROM inventory_types WHERE name = ? COLLATE NOCASE", (type_name,))
if result.rowcount == 0:
return jsonify({"error": "Inventory type not found."}), 404
return jsonify({"deleted": True})
@app.get("/api/summary")
@login_required
def summary():