From 788ef2caf243744e00c0ee6ac23cede343b7055f Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 16 Aug 2026 00:40:33 +0200 Subject: [PATCH] Web: Zweiteinheit erfassen und anzeigen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Artikel-Formular eine Zeile "3 [Stk] ≙ 250 [Gramm]" mit ausgerechneter Kontrolle darunter ("1 Stk ≈ 83,33 g"). Links steht die Basiseinheit des Artikels fest, rechts stehen nur die anderen Arten zur Wahl - eine Bruecke auf die eigene Art haette nichts umzurechnen. Nur vollstaendig ausgefuellt wird sie gespeichert; Gegenstaende ohne Charge tragen keine. buildUnitOptions laesst bei hinterlegter Bruecke auch die Einheiten der Gegenart zu - damit erben Ein- und Auslagern die Faehigkeit ohne eigene Logik. Beim Auslagern steht der Bestand jetzt in beiden Lesarten ("6 Stueck · 500 g"). Co-Authored-By: Claude Opus 5 --- web/src/pages/CheckOut.jsx | 6 +++- web/src/pages/ProductForm.jsx | 63 +++++++++++++++++++++++++++++++++-- web/src/units.js | 38 ++++++++++++++++++++- 3 files changed, 103 insertions(+), 4 deletions(-) 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".