bollwerk/import-inventar.py
Jens Reinemann bed233521e feat: Krisenvorrats-Inventar digitalisiert und Import-Skript erstellt
- inventar.md: 38 Artikel aus handschriftlicher Liste erfasst (Lebensmittel,
  Medikamente, Ausrüstung, Hygiene, Energie)
- import-inventar.py: Python-Skript zum einmaligen PUT /api/inventory
  (UTF-8-safe via ensure_ascii=True, kein PowerShell-Encoding-Problem)
- import-inventar.ps1: PowerShell-Variante (Umlaute via [char]-Variablen)
2026-05-17 23:21:07 +02:00

173 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Importiert das Bollwerk-Vorratsinventar in den Server (einmaliger PUT).
Verwendet App-Default-Kategorien (IDs 1-7) und Default-Location Keller (ID 1).
Aufruf:
python import-inventar.py
python import-inventar.py --url http://localhost:8080
"""
import json
import getpass
import time
import uuid
import argparse
import urllib.request
import urllib.error
BASE_URL = "https://bollwerk.online"
ADMIN_USER = "admin"
def api(method, path, body=None, token=None):
url = BASE_URL + path
# ensure_ascii=True kodiert alle Umlaute als \uXXXX kein Encoding-Problem
data = json.dumps(body, ensure_ascii=True).encode("ascii") if body else None
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def new_item(name, cat_id, qty, unit, expiry=None, kcal=None, notes=""):
now = int(time.time() * 1000)
return {
"id": str(uuid.uuid4()),
"name": name,
"categoryId": cat_id,
"quantity": float(qty),
"unit": unit,
"unitPrice": 0.0,
"kcalPerUnit": kcal,
"expiryDate": expiry,
"locationId": 1,
"notes": notes or "",
"lastUpdated": now,
}
# App-Default-Kategorien (IDs aus SeedDatabaseUseCase)
CATEGORIES = [
{"id": 1, "name": "Lebensmittel"},
{"id": 2, "name": "Wasser"},
{"id": 3, "name": "Medikamente"},
{"id": 4, "name": "Ausrüstung"},
{"id": 5, "name": "Hygiene"},
{"id": 6, "name": "Energie & Licht"},
{"id": 7, "name": "Dokumente"},
]
# App-Default-Lagerort
LOCATIONS = [{"id": 1, "name": "Keller"}]
ITEMS = [
# --- Lebensmittel: Getreide & Hülsenfrüchte (cat 1) ---------------------
new_item("Dinkelkorn", 1, 16, "Beutel", None, None, "Jahrgang 2019, vakuumiert"),
new_item("Weizen (Naturland)", 1, 36, "kg", None, None, "Jahrgang 2023"),
new_item("Dinkel", 1, 40, "kg", None, None, "Jahrgang 2023"),
new_item("Bio-Weizen", 1, 18, "kg", None, None, "Jahrgang 2019"),
new_item("Bio-Weizen (Neu)", 1, 200, "kg", None, None, "Jahrgang 05/2026, Kunststoffbehälter, O2-Absorber, Trockenmittel, lose luftdicht"),
new_item("Grüne Erbsen", 1, 10, "kg", None, None, "Jahrgang 2019"),
new_item("Gelbe Schälerbsen", 1, 10, "kg", None, None, "Jahrgang 2019"),
new_item("Berglinsen (1 kg)", 1, 1, "Pkg.", "2021-12-31", None, "MHD abgelaufen"),
new_item("Berglinsen (500 g)", 1, 4, "Pkg.", "2023-12-31", None, "MHD abgelaufen"),
new_item("Weiße Bohnen", 1, 2, "Pkg.", "2022-12-31", None, "MHD abgelaufen"),
new_item("Jodsalz", 1, 25, "kg", None, None, ""),
# --- Lebensmittel: Konserven (cat 1) ------------------------------------
new_item("Kondensmilch gezuckert (Dovgan)", 1, 88, "Dosen", None, None, "MHD prüfen"),
new_item("Corned Beef (Exeter)", 1, 48, "Dosen", None, None, "MHD prüfen"),
new_item("Geschälte Tomaten", 1, 5, "Dosen", None, None, "2x 2,5 kg, MHD abgelaufen"),
new_item("Tomatenmark (200 g)", 1, 24, "Dosen", None, None, "MHD abgelaufen"),
new_item("Sardinen", 1, 5, "Dosen", None, None, "MHD abgelaufen"),
new_item("Heringsfilet in Tomatensoße", 1, 11, "Dosen", None, None, "MHD abgelaufen"),
new_item("Thunfisch groß (900 g)", 1, 3, "Dosen", None, None, "MHD abgelaufen"),
new_item("Thunfisch klein", 1, 9, "Dosen", None, None, "MHD abgelaufen"),
new_item("Brathering Filet", 1, 2, "Dosen", None, None, "MHD abgelaufen"),
new_item("Dosenbrot", 1, 6, "Dosen", None, None, "MHD abgelaufen"),
# --- Lebensmittel: Öle & Fette (cat 1) ----------------------------------
new_item("Olivenöl (750 ml)", 1, 3, "Flaschen", "2022-12-31", None, "MHD abgelaufen"),
new_item("Avocadoöl (250 ml)", 1, 3, "Flaschen", "2026-12-31", None, "MHD 2026, prüfen"),
new_item("Sonnenblumenöl (1 L)", 1, 3, "Flaschen", "2021-12-31", None, "MHD abgelaufen"),
new_item("Sonnenblumenöl (500 ml)", 1, 1, "Flaschen", "2023-12-31", None, "MHD abgelaufen"),
# --- Lebensmittel: Getränke (cat 1) -------------------------------------
new_item("Instant-Kaffee", 1, 12, "Stk.", "2024-12-31", None, "MHD abgelaufen"),
new_item("Trek'N'Eat Peronin", 1, 8, "Stk.", "2024-12-31", None, "MHD abgelaufen"),
# --- Lebensmittel: Notfallrationen (cat 1) ------------------------------
new_item("Conserva-Set", 1, 30, "Manntage", None, 2300, "à 2300 kcal/Tag, conserva.de"),
new_item("NRG-5 Katadyn (Neubestand)", 1, 96, "Tagesrationen", "2036-04-30", 2300, "4x24, Produktion April 2026"),
new_item("NRG-5 Katadyn (Altbestand)", 1, 19, "Tagesrationen", "2029-01-31", 2300, "Produktion 2019, 5 Rationen verbraucht"),
# --- Medikamente (cat 3) ------------------------------------------------
new_item("Vitamine", 3, 960, "Stk.", None, None, "3x240 + 2x120 Tabletten"),
# --- Ausrüstung (cat 4) -------------------------------------------------
new_item("PMR-Funkgerät (Midland G9 Pro)", 4, 2, "Stk.", None, None, ""),
# --- Hygiene (cat 5) ----------------------------------------------------
new_item("Shampoo Mini", 5, 50, "Stk.", None, None, "à 20 ml"),
new_item("Ethanol", 5, 30, "Liter", None, None, ""),
new_item("Isopropanol", 5, 4, "Liter", None, None, ""),
new_item("Methanol", 5, 1, "Liter", None, None, ""),
new_item("Desinfektionsmittel", 5, 5, "Liter", None, None, ""),
# --- Energie & Licht (cat 6) --------------------------------------------
new_item("Petroleum", 6, 50, "Liter", None, None, ""),
]
def main():
global BASE_URL
parser = argparse.ArgumentParser()
parser.add_argument("--url", default=BASE_URL)
parser.add_argument("--user", default=ADMIN_USER)
args = parser.parse_args()
BASE_URL = args.url.rstrip("/")
password = getpass.getpass(f"Admin-Passwort für '{args.user}' @ {BASE_URL}: ")
print("Logge ein...", end=" ", flush=True)
auth = api("POST", "/api/auth/login", {"username": args.user, "password": password})
token = auth["accessToken"]
print("OK")
print("Prüfe vorhandenes Inventar...", end=" ", flush=True)
existing = api("GET", "/api/inventory", token=token)
count = len(existing.get("items", []))
print(f"{count} Artikel vorhanden.")
if count > 0:
confirm = input(f" WARNUNG: {count} Artikel vorhanden. Überschreiben? (ja/nein): ")
if confirm.strip().lower() != "ja":
print("Abgebrochen.")
return
inventory = {
"version": 1,
"categories": CATEGORIES,
"locations": LOCATIONS,
"items": ITEMS,
"settings": [],
"deletedItemIds": [],
}
print(f"Lade {len(ITEMS)} Artikel hoch...", end=" ", flush=True)
result = api("PUT", "/api/inventory", inventory, token=token)
print("OK")
print()
print("Fertig!")
print(f" Artikel : {len(result.get('items', []))}")
print(f" Kategorien : {len(result.get('categories', []))}")
print(f" Lagerorte : {len(result.get('locations', []))}")
if __name__ == "__main__":
main()