diff --git a/web/src/pages/CheckOut.jsx b/web/src/pages/CheckOut.jsx
index 64f428d..7634c0d 100644
--- a/web/src/pages/CheckOut.jsx
+++ b/web/src/pages/CheckOut.jsx
@@ -4,7 +4,7 @@ import Icon from "../components/Icon";
import { ProduktThumb } from "../components/ProduktBild";
import { useToast } from "../toast";
import { useSettings } from "../settings";
-import { buildUnitOptions, fmt, gebinde, isExpired, unitShort } from "../units";
+import { buildUnitOptions, fmt, gebinde, isExpired, unitShort, zweitText } from "../units";
export default function CheckOut() {
const { formatBestBefore } = useSettings();
@@ -149,6 +149,10 @@ export default function CheckOut() {
{product.name}
Verfügbar: {fmt(product.stock / (product.unit_factor || 1))} {product.unit_name}
+ {/* Zweite Lesart, wenn eine Zweiteinheit hinterlegt ist. */}
+ {zweitText(product.stock || 0, product) && (
+ <> · {zweitText(product.stock || 0, product)}>
+ )}
diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx
index ba85822..8fdf7aa 100644
--- a/web/src/pages/ProductForm.jsx
+++ b/web/src/pages/ProductForm.jsx
@@ -18,14 +18,15 @@ import Einzelstuecke from "../components/Einzelstuecke";
import SplitLotDialog from "../components/SplitLotDialog";
import { locationOptions, locationPathById } from "../locationPath";
import {
- daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired, relativeExpiry,
- toMonthInput, unitShort,
+ BASE_UNITS, daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired,
+ relativeExpiry, toMonthInput, unitShort, zweitFaktor,
} from "../units";
const EMPTY = {
barcode: "", name: "", brand: "", image_url: "",
unit_id: "", package_size: "", package_label: "", date_precision: "day", min_stock: "",
group_id: "", category_id: "", shop_id: "", product_url: "", individual: false, bulk: false,
+ secondary_base: "", secondary_count: "", secondary_amount: "",
};
// (Gebinde kommen jetzt aus der Verwaltung, siehe api.listPackageTypes)
@@ -429,6 +430,10 @@ export default function ProductForm() {
product_url: p.product_url || "",
individual: Boolean(p.individual),
bulk: Boolean(p.bulk),
+ // Zweiteinheit so, wie sie eingegeben wurde („3 ≙ 250").
+ secondary_base: p.secondary_base || "",
+ secondary_count: p.secondary_count ?? "",
+ secondary_amount: p.secondary_amount ?? "",
});
setFieldValues(p.field_values || {});
setModus(p.tracking === "object" ? "object" : "food");
@@ -500,6 +505,14 @@ export default function ProductForm() {
package_label: form.package_label.trim() || null,
date_precision: form.date_precision || "day",
group_id: form.group_id === "" ? null : Number(form.group_id),
+ // Zweiteinheit nur vollstaendig – eine halbe Angabe waere keine Bruecke.
+ ...(form.secondary_base && form.secondary_count !== "" && form.secondary_amount !== ""
+ ? {
+ secondary_base: form.secondary_base,
+ secondary_count: Number(form.secondary_count),
+ secondary_amount: Number(form.secondary_amount),
+ }
+ : { secondary_base: null }),
...minStock,
...(isObject ? { field_values: fieldPayload } : {}),
};
@@ -517,6 +530,8 @@ export default function ProductForm() {
min_stock: null,
min_stock_unit_id: null,
min_stock_in_packages: false,
+ // Zweiteinheit gibt es nur bei Charge-Artikeln.
+ secondary_base: null,
};
}
@@ -573,6 +588,20 @@ export default function ProductForm() {
const readOnly = !isAdmin;
const BASE_SHORT_OF_KIND = { count: "Stk", weight: "g", volume: "ml" };
+ // Basiseinheit je Art – die eigene Art fliegt aus der Zweiteinheit-Auswahl.
+ const BASE_OF_KIND = { count: "piece", weight: "gram", volume: "milliliter" };
+ // Ausgerechneter Wert als Kontrolle: „1 Stk ≈ 83,33 g".
+ const zweitHinweis = (() => {
+ const f = zweitFaktor({
+ secondary_base: form.secondary_base,
+ secondary_count: form.secondary_count,
+ secondary_amount: form.secondary_amount,
+ });
+ if (!f) return "";
+ return `1 ${BASE_SHORT_OF_KIND[selectedUnit?.kind] || "Einheit"} \u2248 `
+ + `${fmt(f)} ${unitShort(form.secondary_base)}. `
+ + "Bestände bleiben beim Ändern unverändert – nur ihre Umrechnung verschiebt sich.";
+ })();
const baseShort = selectedUnit
? BASE_SHORT_OF_KIND[selectedUnit.kind]
: product ? unitShort(product.base_unit) : "";
@@ -819,6 +848,36 @@ export default function ProductForm() {
+ {/* Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen.
+ Links steht die Basiseinheit des Artikels fest, rechts die
+ Gegenart. Ohne Eintrag bleibt alles wie bisher getrennt. */}
+
+
+
+ {zweitHinweis &&
{zweitHinweis}
}
{/* Beim Anlegen gibt es den Artikel noch nicht, also auch keine
Ort-Liste – hier bleibt das eine Feld, das als „Überall“ landet.
diff --git a/web/src/units.js b/web/src/units.js
index 8ce544c..0bc828f 100644
--- a/web/src/units.js
+++ b/web/src/units.js
@@ -54,6 +54,37 @@ export function kindShort(kind) {
return KIND_SHORT[kind] || "";
}
+// Einheiten-ART je Basiseinheit – Spiegelbild von conversion.py::KIND_OF_BASE.
+export const KIND_OF_BASE = { piece: "count", gram: "weight", milliliter: "volume" };
+
+// ---- Zweiteinheit: die Brücke zwischen den Einheiten-Arten ----
+// Ein Artikel darf zusätzlich zu seiner Basiseinheit eine Äquivalenz tragen:
+// „3 Stück ≙ 250 g". Der Server liefert den ausgerechneten Faktor als
+// `secondary_factor` mit; das Paar dahinter bleibt zur Anzeige erhalten.
+
+/** Zweit-Basiseinheiten je EINER Basiseinheit. 0 = keine Brücke hinterlegt. */
+export function zweitFaktor(p) {
+ if (!p?.secondary_base) return 0;
+ const f = Number(p.secondary_factor);
+ if (f > 0) return f;
+ // Rückfall, falls das abgeleitete Feld fehlt (ältere Antwort).
+ const n = Number(p.secondary_count);
+ const m = Number(p.secondary_amount);
+ return n > 0 && m > 0 ? m / n : 0;
+}
+
+/** Eine Menge (Basiseinheiten) in der Zweiteinheit lesen: „500 g". */
+export function zweitText(mengeBase, p) {
+ const f = zweitFaktor(p);
+ return f ? `${fmt(mengeBase * f)} ${unitShort(p.secondary_base)}` : "";
+}
+
+/** Bestand mit beiden Lesarten: „6 Stück · 500 g". */
+export function stockLabelMitZweit(p) {
+ const zweit = zweitText(p.stock || 0, p);
+ return zweit ? `${stockLabel(p)} · ${zweit}` : stockLabel(p);
+}
+
// Auswahlmöglichkeiten für Menge beim Ein-/Auslagern eines Produkts.
export function unitOptions(product) {
const opts = [{ value: baseUnitToInput(product.base_unit), label: unitShort(product.base_unit) }];
@@ -194,8 +225,13 @@ export function amountText(qty, packageSize, baseUnit) {
// Einheiten-Optionen fürs Ein-/Auslagern: verwaltete Einheiten der Produkt-Art + Packung.
export function buildUnitOptions(product, units) {
+ // Ohne Zweiteinheit nur die eigene Art. Mit Brücke kommen die Einheiten der
+ // Gegenart dazu – das Backend rechnet sie über die am Artikel hinterlegte
+ // Äquivalenz um (services/conversion.py::to_base).
+ const erlaubt = new Set([product.kind]);
+ if (zweitFaktor(product)) erlaubt.add(KIND_OF_BASE[product.secondary_base]);
const opts = (units || [])
- .filter((u) => u.kind === product.kind)
+ .filter((u) => erlaubt.has(u.kind))
.map((u) => ({ value: u.name, label: u.name }));
if (product.package_size) {
// Bezeichnung des Gebindes je Artikel (Glas, Tüte, …); Wert bleibt "package".