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:
@@ -4,7 +4,7 @@ import { api } from "../api";
|
||||
import { useAuth } from "../auth";
|
||||
import Icon from "../components/Icon";
|
||||
import { guessGroup, suggestionToProduct } from "../offUtils";
|
||||
import { fmt, isExpired, unitOptions, unitShort } from "../units";
|
||||
import { buildUnitOptions, fmt, isExpired } from "../units";
|
||||
|
||||
const emptyLine = () => ({ quantity: "", best_before: "" });
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function CheckIn() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [units, setUnits] = useState([]);
|
||||
|
||||
const [unit, setUnit] = useState("");
|
||||
const [locationId, setLocationId] = useState("");
|
||||
@@ -30,6 +31,7 @@ export default function CheckIn() {
|
||||
useEffect(() => {
|
||||
api.listLocations().then(setLocations).catch(() => {});
|
||||
api.listGroups().then(setGroups).catch(() => {});
|
||||
api.listUnits().then(setUnits).catch(() => {});
|
||||
}, []);
|
||||
|
||||
function resetLookup() {
|
||||
@@ -41,7 +43,8 @@ export default function CheckIn() {
|
||||
setProduct(p);
|
||||
setResults([]);
|
||||
resetLookup();
|
||||
setUnit(unitOptions(p)[0].value);
|
||||
const opts = buildUnitOptions(p, units);
|
||||
setUnit(p.unit_name || (opts[0] && opts[0].value) || "");
|
||||
setLines([emptyLine()]);
|
||||
setLocationId("");
|
||||
}
|
||||
@@ -118,7 +121,7 @@ export default function CheckIn() {
|
||||
const res = await api.checkInBatch({ product_id: product.id, unit, lines: payloadLines });
|
||||
setInfo(
|
||||
`${payloadLines.length} Charge(n) eingelagert. Neuer Bestand: ` +
|
||||
`${fmt(res.product_stock)} ${unitShort(product.base_unit)}`
|
||||
`${fmt(res.product_stock / (product.unit_factor || 1))} ${product.unit_name}`
|
||||
);
|
||||
setLines([emptyLine()]);
|
||||
setProduct(await api.getProduct(product.id));
|
||||
@@ -197,7 +200,7 @@ export default function CheckIn() {
|
||||
{results.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button className="link-btn" onClick={() => selectProduct(p)}>
|
||||
{p.name} <span className="muted">({fmt(p.stock)} {unitShort(p.base_unit)})</span>
|
||||
{p.name} <span className="muted">({fmt(p.stock / (p.unit_factor || 1))} {p.unit_name})</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -214,7 +217,7 @@ export default function CheckIn() {
|
||||
: <span className="thumb thumb-fallback"><Icon name="box" size={16} /></span>}
|
||||
<div className="info">
|
||||
<div className="title">{product.name}</div>
|
||||
<div className="muted small">Bestand: {fmt(product.stock)} {unitShort(product.base_unit)}</div>
|
||||
<div className="muted small">Bestand: {fmt(product.stock / (product.unit_factor || 1))} {product.unit_name}</div>
|
||||
</div>
|
||||
<button type="button" className="btn ghost" onClick={() => setProduct(null)}>Ändern</button>
|
||||
</div>
|
||||
@@ -223,7 +226,7 @@ export default function CheckIn() {
|
||||
<label className="grow">
|
||||
Einheit
|
||||
<select value={unit} onChange={(e) => setUnit(e.target.value)}>
|
||||
{unitOptions(product).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
{buildUnitOptions(product, units).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grow">
|
||||
@@ -274,7 +277,7 @@ export default function CheckIn() {
|
||||
</button>
|
||||
{totalQty > 0 && (
|
||||
<span className="muted small">Summe: {fmt(totalQty)} {
|
||||
unitOptions(product).find((o) => o.value === unit)?.label || ""
|
||||
buildUnitOptions(product, units).find((o) => o.value === unit)?.label || ""
|
||||
}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user