Add dynamic inventory type creation
This commit is contained in:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user