Verwaltbare Einheiten mit Umrechnung + Chargen bearbeiten + gezieltes Auslagern

Einheiten (neu):
- Tabelle units (name, kind=count|weight|volume, factor, is_builtin); eingebaut
  Stueck/Gramm/Kilogramm/Milliliter/Liter, Admin kann eigene anlegen (z.B. Pfund=500g).
- Bestaende bleiben intern in kanonischer Basis (Stueck/Gramm/Milliliter);
  Product.display_unit_id und Group.min_stock_unit_id als nullable FKs.
- Neuer Umrechnungs-Service (services/conversion.py) ersetzt die feste Einheitenlogik;
  Ein-/Auslagern und Gruppen-Mindestbestand rechnen ueber den Faktor.
- Gruppen-Mindestbestand mit Einheit; Gruppenbestand summiert nur Produkte
  passender Art. Neue Verwaltungsseite "Einheiten" (Admin).
- Schonende Migration beim Start: ADD COLUMN IF NOT EXISTS (Postgres), damit
  bestehende Installationen ihre Daten behalten.

Chargen:
- PATCH /lots/{id} und DELETE /lots/{id}: Menge/MHD korrigieren, Charge loeschen
  (wird als Korrektur-Bewegung protokolliert). Bearbeitung im Produktdetail.

Auslagern:
- Optionales lot_id: gezielt aus einer bestimmten Charge/MHD abbuchen statt FEFO;
  Auswahl-Dropdown in der Auslagern-Seite.

Sonstiges: Roadmap aktualisiert, Tests fuer die Umrechnung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 12:33:14 +02:00
parent d4e7ff8e3d
commit 0310cdfbd6
26 changed files with 1015 additions and 177 deletions

107
web/src/pages/Units.jsx Normal file
View File

@@ -0,0 +1,107 @@
import { useEffect, useState } from "react";
import { api } from "../api";
import Icon from "../components/Icon";
import { fmt } from "../units";
const KIND_LABEL = { count: "Anzahl (Stück)", weight: "Gewicht (Basis: Gramm)", volume: "Volumen (Basis: Milliliter)" };
export default function Units() {
const [units, setUnits] = useState([]);
const [form, setForm] = useState({ name: "", kind: "weight", factor: "" });
const [error, setError] = useState(null);
async function load() {
try {
setUnits(await api.listUnits());
} catch (err) {
setError(err.message);
}
}
useEffect(() => { load(); }, []);
async function add(e) {
e.preventDefault();
setError(null);
try {
await api.createUnit({ name: form.name, kind: form.kind, factor: Number(form.factor) });
setForm({ name: "", kind: form.kind, factor: "" });
load();
} catch (err) {
setError(err.message);
}
}
async function remove(u) {
if (!confirm(`Einheit "${u.name}" löschen?`)) return;
try {
await api.deleteUnit(u.id);
load();
} catch (err) {
setError(err.message);
}
}
const baseUnitOfKind = { count: "Stück", weight: "Gramm", volume: "Milliliter" };
return (
<div>
<div className="page-head">
<div>
<h1>Einheiten</h1>
<div className="sub">Faktor = wie viele Basiseinheiten 1 dieser Einheit entsprechen</div>
</div>
</div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
<div className="grid-2">
<div className="card" style={{ padding: 0 }}>
<div className="table-wrap">
<table className="table">
<thead><tr><th>Name</th><th>Art</th><th className="num">Faktor</th><th></th></tr></thead>
<tbody>
{units.map((u) => (
<tr key={u.id}>
<td className="strong">{u.name}{u.is_builtin && <span className="badge" style={{ marginLeft: 6 }}>eingebaut</span>}</td>
<td className="muted">{KIND_LABEL[u.kind] || u.kind}</td>
<td className="num">{fmt(u.factor)} {baseUnitOfKind[u.kind]}</td>
<td className="num">
{!u.is_builtin && (
<button className="btn-icon danger" onClick={() => remove(u)} title="Löschen">
<Icon name="trash" size={16} />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<form className="card" onSubmit={add}>
<div className="card-head"><Icon name="settings" /><h2>Neue Einheit</h2></div>
<label>
Name
<input placeholder="z.B. Pfund" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required />
</label>
<label>
Art
<select value={form.kind} onChange={(e) => setForm({ ...form, kind: e.target.value })}>
<option value="count">Anzahl (Stück)</option>
<option value="weight">Gewicht (Basis: Gramm)</option>
<option value="volume">Volumen (Basis: Milliliter)</option>
</select>
</label>
<label>
Faktor zur Basiseinheit
<input type="number" step="any" min="0" value={form.factor}
onChange={(e) => setForm({ ...form, factor: e.target.value })} required
placeholder={`z.B. 500 (= 500 ${baseUnitOfKind[form.kind]})`} />
</label>
<button className="btn primary"><Icon name="plus" size={16} />Anlegen</button>
</form>
</div>
</div>
);
}