Fix Einkaufsliste: Mindestbestand in Packungen angebbar (Einheiten-Missverstaendnis)
Problem: min_stock wird in Basiseinheiten verglichen. Bei einem 190g-Glas (base gram, package 190) bedeutete "min 2" 2 Gramm statt 2 Glaeser -> Produkt tauchte nie auf der Einkaufsliste auf. Fix: - Produktformular: Mindestbestand wahlweise in Basiseinheit ODER Packungen eingebbar (mit Umrechnung); Default Packungen bei Packungsprodukten. - ShoppingItem liefert package_size; Einkaufsliste/Dashboard zeigen Bedarf in Packungen (+ Basiseinheit in Klammern) via amountText(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ def shopping_list(
|
|||||||
product_id=product.id,
|
product_id=product.id,
|
||||||
name=product.name,
|
name=product.name,
|
||||||
base_unit=product.base_unit,
|
base_unit=product.base_unit,
|
||||||
|
package_size=product.package_size,
|
||||||
stock=stock,
|
stock=stock,
|
||||||
min_stock=product.min_stock,
|
min_stock=product.min_stock,
|
||||||
deficit=product.min_stock - stock,
|
deficit=product.min_stock - stock,
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class ShoppingItem(BaseModel):
|
|||||||
product_id: int
|
product_id: int
|
||||||
name: str
|
name: str
|
||||||
base_unit: BaseUnit
|
base_unit: BaseUnit
|
||||||
|
package_size: float | None = None
|
||||||
stock: float
|
stock: float
|
||||||
min_stock: float
|
min_stock: float
|
||||||
deficit: float
|
deficit: float
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { fmt, unitShort } from "../units";
|
import { amountText, fmt, unitShort } from "../units";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [expiring, setExpiring] = useState([]);
|
const [expiring, setExpiring] = useState([]);
|
||||||
@@ -115,8 +115,8 @@ export default function Dashboard() {
|
|||||||
{shopping.map((it) => (
|
{shopping.map((it) => (
|
||||||
<tr key={`p${it.product_id}`}>
|
<tr key={`p${it.product_id}`}>
|
||||||
<td>{it.name}</td>
|
<td>{it.name}</td>
|
||||||
<td className="num">{fmt(it.stock)} {unitShort(it.base_unit)}</td>
|
<td className="num">{amountText(it.stock, it.package_size, it.base_unit)}</td>
|
||||||
<td className="num strong">{fmt(it.deficit)} {unitShort(it.base_unit)}</td>
|
<td className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -25,11 +25,23 @@ export default function ProductForm() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
// Einheit, in der der Mindestbestand eingegeben wird: "base" oder "package".
|
||||||
|
const [minUnit, setMinUnit] = useState("base");
|
||||||
|
|
||||||
function set(k, v) {
|
function set(k, v) {
|
||||||
setForm((f) => ({ ...f, [k]: v }));
|
setForm((f) => ({ ...f, [k]: v }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wechselt die Mindestbestand-Einheit und rechnet den angezeigten Wert um.
|
||||||
|
function changeMinUnit(newUnit) {
|
||||||
|
const ps = Number(form.package_size);
|
||||||
|
if (form.min_stock !== "" && ps > 0 && newUnit !== minUnit) {
|
||||||
|
const v = Number(form.min_stock);
|
||||||
|
set("min_stock", String(newUnit === "package" ? v / ps : v * ps));
|
||||||
|
}
|
||||||
|
setMinUnit(newUnit);
|
||||||
|
}
|
||||||
|
|
||||||
function applySuggestion(s, groupsList) {
|
function applySuggestion(s, groupsList) {
|
||||||
const groupGuess = guessGroup(groupsList, s);
|
const groupGuess = guessGroup(groupsList, s);
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
@@ -42,6 +54,7 @@ export default function ProductForm() {
|
|||||||
package_size: s.package_size != null ? String(s.package_size) : f.package_size,
|
package_size: s.package_size != null ? String(s.package_size) : f.package_size,
|
||||||
group_id: f.group_id || groupGuess,
|
group_id: f.group_id || groupGuess,
|
||||||
}));
|
}));
|
||||||
|
if (s.package_size != null) setMinUnit("package");
|
||||||
setInfo(
|
setInfo(
|
||||||
"Daten von Open Food Facts übernommen." +
|
"Daten von Open Food Facts übernommen." +
|
||||||
(s.package_size != null ? "" : " (Füllmenge nicht hinterlegt – bitte Packungsgröße prüfen.)") +
|
(s.package_size != null ? "" : " (Füllmenge nicht hinterlegt – bitte Packungsgröße prüfen.)") +
|
||||||
@@ -76,10 +89,19 @@ export default function ProductForm() {
|
|||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
const p = await api.getProduct(id);
|
const p = await api.getProduct(id);
|
||||||
setProduct(p);
|
setProduct(p);
|
||||||
|
// Mindestbestand ist in Basiseinheiten gespeichert; bei Packungsprodukten
|
||||||
|
// zeigen wir ihn zur besseren Verständlichkeit in Packungen an.
|
||||||
|
let minDisplay = p.min_stock ?? "";
|
||||||
|
if (p.min_stock != null && p.package_size && p.package_size > 0) {
|
||||||
|
minDisplay = p.min_stock / p.package_size;
|
||||||
|
setMinUnit("package");
|
||||||
|
} else {
|
||||||
|
setMinUnit("base");
|
||||||
|
}
|
||||||
setForm({
|
setForm({
|
||||||
barcode: p.barcode || "", name: p.name, brand: p.brand || "",
|
barcode: p.barcode || "", name: p.name, brand: p.brand || "",
|
||||||
image_url: p.image_url || "", base_unit: p.base_unit,
|
image_url: p.image_url || "", base_unit: p.base_unit,
|
||||||
package_size: p.package_size ?? "", min_stock: p.min_stock ?? "",
|
package_size: p.package_size ?? "", min_stock: minDisplay,
|
||||||
group_id: p.group_id ?? "",
|
group_id: p.group_id ?? "",
|
||||||
});
|
});
|
||||||
setLots(await api.listLots(id));
|
setLots(await api.listLots(id));
|
||||||
@@ -99,6 +121,12 @@ export default function ProductForm() {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
|
const ps = Number(form.package_size);
|
||||||
|
let minBase = null;
|
||||||
|
if (form.min_stock !== "") {
|
||||||
|
const v = Number(form.min_stock);
|
||||||
|
minBase = minUnit === "package" && ps > 0 ? v * ps : v;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
barcode: form.barcode || null,
|
barcode: form.barcode || null,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
@@ -106,7 +134,7 @@ export default function ProductForm() {
|
|||||||
image_url: form.image_url || null,
|
image_url: form.image_url || null,
|
||||||
base_unit: form.base_unit,
|
base_unit: form.base_unit,
|
||||||
package_size: form.package_size === "" ? null : Number(form.package_size),
|
package_size: form.package_size === "" ? null : Number(form.package_size),
|
||||||
min_stock: form.min_stock === "" ? null : Number(form.min_stock),
|
min_stock: minBase,
|
||||||
group_id: form.group_id === "" ? null : Number(form.group_id),
|
group_id: form.group_id === "" ? null : Number(form.group_id),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -195,8 +223,16 @@ export default function ProductForm() {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<label className="grow">
|
<label className="grow">
|
||||||
Mindestbestand
|
Mindestbestand
|
||||||
|
<div className="field-inline">
|
||||||
<input type="number" step="any" value={form.min_stock}
|
<input type="number" step="any" value={form.min_stock}
|
||||||
onChange={(e) => set("min_stock", e.target.value)} disabled={readOnly} />
|
onChange={(e) => set("min_stock", e.target.value)} disabled={readOnly}
|
||||||
|
placeholder="z.B. 2" />
|
||||||
|
<select value={minUnit} onChange={(e) => changeMinUnit(e.target.value)}
|
||||||
|
disabled={readOnly} style={{ maxWidth: 140, marginTop: 0 }}>
|
||||||
|
<option value="base">{unitShort(form.base_unit)}</option>
|
||||||
|
{form.package_size && <option value="package">Packung(en)</option>}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label className="grow">
|
<label className="grow">
|
||||||
Gruppe
|
Gruppe
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { fmt, unitShort } from "../units";
|
import { amountText, fmt, unitShort } from "../units";
|
||||||
|
|
||||||
export default function ShoppingList() {
|
export default function ShoppingList() {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
@@ -60,8 +60,8 @@ export default function ShoppingList() {
|
|||||||
<span className="item-name">{it.name}</span>
|
<span className="item-name">{it.name}</span>
|
||||||
</label>
|
</label>
|
||||||
<span className="muted small">
|
<span className="muted small">
|
||||||
fehlt <strong>{fmt(it.deficit)} {unitShort(it.base_unit)}</strong>{" "}
|
fehlt <strong>{amountText(it.deficit, it.package_size, it.base_unit)}</strong>{" "}
|
||||||
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})
|
(Bestand {amountText(it.stock, it.package_size, it.base_unit)})
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ export function fmt(n) {
|
|||||||
if (n == null) return "–";
|
if (n == null) return "–";
|
||||||
return Number(n).toLocaleString("de-DE", { maximumFractionDigits: 2 });
|
return Number(n).toLocaleString("de-DE", { maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Menge lesbar darstellen: bei Packungsprodukten in Packungen (+ Basiseinheit in Klammern).
|
||||||
|
export function amountText(qty, packageSize, baseUnit) {
|
||||||
|
if (packageSize && packageSize > 0) {
|
||||||
|
return `${fmt(qty / packageSize)} Pkg (${fmt(qty)} ${unitShort(baseUnit)})`;
|
||||||
|
}
|
||||||
|
return `${fmt(qty)} ${unitShort(baseUnit)}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user