App: Zweiteinheit erfassen und anzeigen
Eigener Abschnitt im Artikel mit "Menge [Stk] entspricht [Gramm] das sind
[250]" und der ausgerechneten Kontrolle in der Fusszeile. Nur Charge-Artikel,
und nur die anderen Arten stehen zur Wahl - eine Bruecke auf die eigene Art
haette nichts umzurechnen.
ProductUpdateRequest bekommt sendSecondary nach dem Vorbild von sendMinStock:
ein PATCH aus einer anderen Maske soll die Bruecke nicht unbeabsichtigt
loeschen. Die Einheitenauswahl beim Ein- und Auslagern nimmt bei hinterlegter
Bruecke die Gegenart mit auf, der Bestand steht in beiden Lesarten
("6 Stueck · 500 g").
Alle neuen Product-Felder sind Optionals, damit die App auch gegen einen
aelteren Server laeuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,8 +171,12 @@ struct CheckInFormView: View {
|
|||||||
|
|
||||||
/// Einheiten der passenden Art plus das Gebinde des Artikels.
|
/// Einheiten der passenden Art plus das Gebinde des Artikels.
|
||||||
private var unitOptions: [UnitOption] {
|
private var unitOptions: [UnitOption] {
|
||||||
|
// Bei hinterlegter Zweiteinheit auch die Einheiten der Gegenart – das
|
||||||
|
// Backend rechnet sie ueber die Bruecke am Artikel um.
|
||||||
|
var arten: Set<String> = [product.kind]
|
||||||
|
if product.zweitFaktor != nil, let k = product.zweitKind { arten.insert(k) }
|
||||||
var options = units
|
var options = units
|
||||||
.filter { $0.kind == product.kind }
|
.filter { arten.contains($0.kind) }
|
||||||
.map { UnitOption(value: $0.name, label: $0.name) }
|
.map { UnitOption(value: $0.name, label: $0.name) }
|
||||||
if let size = product.packageSize, size > 0 {
|
if let size = product.packageSize, size > 0 {
|
||||||
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
||||||
|
|||||||
@@ -136,8 +136,12 @@ struct CheckOutFormView: View {
|
|||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
|
|
||||||
private var unitOptions: [UnitOption] {
|
private var unitOptions: [UnitOption] {
|
||||||
|
// Bei hinterlegter Zweiteinheit auch die Einheiten der Gegenart – das
|
||||||
|
// Backend rechnet sie ueber die Bruecke am Artikel um.
|
||||||
|
var arten: Set<String> = [product.kind]
|
||||||
|
if product.zweitFaktor != nil, let k = product.zweitKind { arten.insert(k) }
|
||||||
var options = units
|
var options = units
|
||||||
.filter { $0.kind == product.kind }
|
.filter { arten.contains($0.kind) }
|
||||||
.map { UnitOption(value: $0.name, label: $0.name) }
|
.map { UnitOption(value: $0.name, label: $0.name) }
|
||||||
if let size = product.packageSize, size > 0 {
|
if let size = product.packageSize, size > 0 {
|
||||||
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ struct Product: Codable, Identifiable, Hashable {
|
|||||||
let baseUnit: String
|
let baseUnit: String
|
||||||
let packageSize: Double?
|
let packageSize: Double?
|
||||||
let packageLabel: String?
|
let packageLabel: String?
|
||||||
|
/// Zweiteinheit: „secondaryCount Basiseinheiten ≙ secondaryAmount
|
||||||
|
/// secondaryBase". Bruecke zwischen den sonst strikt getrennten Arten.
|
||||||
|
/// Optionals, damit die App auch gegen einen aelteren Server laeuft.
|
||||||
|
let secondaryBase: String?
|
||||||
|
let secondaryCount: Double?
|
||||||
|
let secondaryAmount: Double?
|
||||||
|
/// Vom Server ausgerechnet: Zweit-Basiseinheiten je EINER Basiseinheit.
|
||||||
|
let secondaryFactor: Double?
|
||||||
let groupId: Int?
|
let groupId: Int?
|
||||||
let minStock: Double?
|
let minStock: Double?
|
||||||
let stock: Double
|
let stock: Double
|
||||||
@@ -98,6 +106,10 @@ struct Product: Codable, Identifiable, Hashable {
|
|||||||
case baseUnit = "base_unit"
|
case baseUnit = "base_unit"
|
||||||
case packageSize = "package_size"
|
case packageSize = "package_size"
|
||||||
case packageLabel = "package_label"
|
case packageLabel = "package_label"
|
||||||
|
case secondaryBase = "secondary_base"
|
||||||
|
case secondaryCount = "secondary_count"
|
||||||
|
case secondaryAmount = "secondary_amount"
|
||||||
|
case secondaryFactor = "secondary_factor"
|
||||||
case groupId = "group_id"
|
case groupId = "group_id"
|
||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
case expiredCount = "expired_count"
|
case expiredCount = "expired_count"
|
||||||
@@ -134,6 +146,38 @@ struct Product: Codable, Identifiable, Hashable {
|
|||||||
|
|
||||||
var stockInArticleUnits: Double { stock / articleUnitFactor }
|
var stockInArticleUnits: Double { stock / articleUnitFactor }
|
||||||
|
|
||||||
|
/// Zweit-Basiseinheiten je EINER Basiseinheit – nil ohne Bruecke.
|
||||||
|
var zweitFaktor: Double? {
|
||||||
|
guard let basis = secondaryBase, !basis.isEmpty, basis != baseUnit else { return nil }
|
||||||
|
if let f = secondaryFactor, f > 0 { return f }
|
||||||
|
// Rueckfall, falls das abgeleitete Feld fehlt (aelterer Server).
|
||||||
|
guard let n = secondaryCount, let m = secondaryAmount, n > 0, m > 0 else { return nil }
|
||||||
|
return m / n
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Art der Zweiteinheit ("count"/"weight"/"volume") – fuer die Einheitenwahl.
|
||||||
|
var zweitKind: String? {
|
||||||
|
switch secondaryBase {
|
||||||
|
case "piece": return "count"
|
||||||
|
case "gram": return "weight"
|
||||||
|
case "milliliter": return "volume"
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kuerzel der Zweit-Basiseinheit fuer die Anzeige.
|
||||||
|
var zweitKurz: String {
|
||||||
|
switch secondaryBase {
|
||||||
|
case "gram": return "g"
|
||||||
|
case "milliliter": return "ml"
|
||||||
|
case "piece": return "Stück"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bestand in der Zweiteinheit, z.B. 500 g bei 6 Stueck.
|
||||||
|
var stockInZweit: Double? { zweitFaktor.map { stock * $0 } }
|
||||||
|
|
||||||
/// Wie es um den Mindestbestand steht. Beides in Basiseinheiten verglichen.
|
/// Wie es um den Mindestbestand steht. Beides in Basiseinheiten verglichen.
|
||||||
enum StockLevel { case none, ok, close, below }
|
enum StockLevel { case none, ok, close, below }
|
||||||
|
|
||||||
@@ -585,6 +629,12 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
var individual: Bool? = nil
|
var individual: Bool? = nil
|
||||||
var bulk: Bool? = nil
|
var bulk: Bool? = nil
|
||||||
var sendMode = false
|
var sendMode = false
|
||||||
|
// Zweiteinheit ebenfalls nur auf Wunsch – ein PATCH aus einer anderen Maske
|
||||||
|
// soll die Bruecke nicht unbeabsichtigt loeschen.
|
||||||
|
var secondaryBase: String? = nil
|
||||||
|
var secondaryCount: Double? = nil
|
||||||
|
var secondaryAmount: Double? = nil
|
||||||
|
var sendSecondary = false
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case name, brand, individual, bulk
|
case name, brand, individual, bulk
|
||||||
@@ -600,6 +650,9 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
case minStockInPackages = "min_stock_in_packages"
|
case minStockInPackages = "min_stock_in_packages"
|
||||||
case minStockUnitId = "min_stock_unit_id"
|
case minStockUnitId = "min_stock_unit_id"
|
||||||
case baseUnit = "base_unit"
|
case baseUnit = "base_unit"
|
||||||
|
case secondaryBase = "secondary_base"
|
||||||
|
case secondaryCount = "secondary_count"
|
||||||
|
case secondaryAmount = "secondary_amount"
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
@@ -625,6 +678,11 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
try container.encode(bulk, forKey: .bulk)
|
try container.encode(bulk, forKey: .bulk)
|
||||||
if let baseUnit { try container.encode(baseUnit, forKey: .baseUnit) }
|
if let baseUnit { try container.encode(baseUnit, forKey: .baseUnit) }
|
||||||
}
|
}
|
||||||
|
if sendSecondary {
|
||||||
|
try container.encode(secondaryBase, forKey: .secondaryBase)
|
||||||
|
try container.encode(secondaryCount, forKey: .secondaryCount)
|
||||||
|
try container.encode(secondaryAmount, forKey: .secondaryAmount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ struct ProductDetailView: View {
|
|||||||
@State private var packageSize = ""
|
@State private var packageSize = ""
|
||||||
@State private var packageLabel = ""
|
@State private var packageLabel = ""
|
||||||
@State private var datePrecision = "day"
|
@State private var datePrecision = "day"
|
||||||
|
// Zweiteinheit („3 Stück ≙ 250 g") – Bruecke zur anderen Art.
|
||||||
|
@State private var zweitAnzahl = ""
|
||||||
|
@State private var zweitMenge = ""
|
||||||
|
@State private var zweitBasis = ""
|
||||||
@State private var groupId: Int?
|
@State private var groupId: Int?
|
||||||
@State private var categoryId: Int?
|
@State private var categoryId: Int?
|
||||||
// Verwaltungsart eines Gegenstands: count | individual | bulk – umstellbar.
|
// Verwaltungsart eines Gegenstands: count | individual | bulk – umstellbar.
|
||||||
@@ -62,6 +66,9 @@ struct ProductDetailView: View {
|
|||||||
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
||||||
if packageLabel != (current.packageLabel ?? "") { return true }
|
if packageLabel != (current.packageLabel ?? "") { return true }
|
||||||
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
|
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
|
||||||
|
if zweitBasis != (current.secondaryBase ?? "") { return true }
|
||||||
|
if zweitAnzahl != (current.secondaryCount.map { formatAmount($0) } ?? "") { return true }
|
||||||
|
if zweitMenge != (current.secondaryAmount.map { formatAmount($0) } ?? "") { return true }
|
||||||
}
|
}
|
||||||
if current.isObject {
|
if current.isObject {
|
||||||
if objMode != currentObjMode { return true }
|
if objMode != currentObjMode { return true }
|
||||||
@@ -97,8 +104,12 @@ struct ProductDetailView: View {
|
|||||||
} else {
|
} else {
|
||||||
// Lebensmittel und Verbrauchsgegenstände: Vorrat aus den Chargen.
|
// Lebensmittel und Verbrauchsgegenstände: Vorrat aus den Chargen.
|
||||||
Section("Bestand") {
|
Section("Bestand") {
|
||||||
LabeledContent("Vorrat",
|
// Mit Zweiteinheit beide Lesarten: „6 Stück · 500 g".
|
||||||
value: "\(formatAmount(current.stockInArticleUnits)) \(current.articleUnitLabel)")
|
LabeledContent("Vorrat", value: {
|
||||||
|
let haupt = "\(formatAmount(current.stockInArticleUnits)) \(current.articleUnitLabel)"
|
||||||
|
guard let zweit = current.stockInZweit else { return haupt }
|
||||||
|
return "\(haupt) · \(formatAmount(zweit)) \(current.zweitKurz)"
|
||||||
|
}())
|
||||||
if current.expiredCount > 0 {
|
if current.expiredCount > 0 {
|
||||||
LabeledContent("Abgelaufen", value: "\(current.expiredCount)")
|
LabeledContent("Abgelaufen", value: "\(current.expiredCount)")
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
@@ -124,6 +135,30 @@ struct ProductDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen.
|
||||||
|
// Damit zaehlt der Artikel auch in Gruppen der anderen Art und
|
||||||
|
// laesst sich darin ein- und auslagern. Das Gebinde bleibt frei.
|
||||||
|
if current.foodLike {
|
||||||
|
Section {
|
||||||
|
QuantityField(label: "Menge", text: $zweitAnzahl,
|
||||||
|
suffix: display.baseUnitLabel(current.baseUnit))
|
||||||
|
Picker("entspricht", selection: $zweitBasis) {
|
||||||
|
Text("– keine –").tag("")
|
||||||
|
ForEach(zweitBasisOptionen, id: \.self) { basis in
|
||||||
|
Text(display.baseUnitLabel(basis)).tag(basis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !zweitBasis.isEmpty {
|
||||||
|
QuantityField(label: "das sind", text: $zweitMenge,
|
||||||
|
suffix: display.baseUnitLabel(zweitBasis))
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Zweiteinheit (optional)")
|
||||||
|
} footer: {
|
||||||
|
Text(zweitHinweis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
CategoryPicker(categories: categories, selection: $categoryId,
|
CategoryPicker(categories: categories, selection: $categoryId,
|
||||||
tracking: current.tracking)
|
tracking: current.tracking)
|
||||||
@@ -344,6 +379,25 @@ struct ProductDetailView: View {
|
|||||||
return teile.joined(separator: " · ")
|
return teile.joined(separator: " · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Basiseinheiten der ANDEREN Arten – eine Bruecke auf die eigene Art
|
||||||
|
/// haette nichts umzurechnen.
|
||||||
|
private var zweitBasisOptionen: [String] {
|
||||||
|
["piece", "gram", "milliliter"].filter { $0 != current.baseUnit }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kontrolle unter der Eingabe: „1 Stück ≈ 83,33 g".
|
||||||
|
private var zweitHinweis: String {
|
||||||
|
let n = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||||
|
let m = Double(zweitMenge.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||||
|
guard !zweitBasis.isEmpty, n > 0, m > 0 else {
|
||||||
|
return "Zusätzliche Lesart desselben Artikels – damit zählt er auch in "
|
||||||
|
+ "Gruppen der anderen Art und lässt sich darin ein- und auslagern."
|
||||||
|
}
|
||||||
|
return "1 \(display.baseUnitLabel(current.baseUnit)) ≈ \(formatAmount(m / n)) "
|
||||||
|
+ "\(display.baseUnitLabel(zweitBasis)). Bestände bleiben beim Ändern "
|
||||||
|
+ "unverändert – nur ihre Umrechnung verschiebt sich."
|
||||||
|
}
|
||||||
|
|
||||||
/// Aktuelle Verwaltungsart des geladenen Artikels.
|
/// Aktuelle Verwaltungsart des geladenen Artikels.
|
||||||
private var currentObjMode: String {
|
private var currentObjMode: String {
|
||||||
current.isIndividual ? "individual" : (current.isBulk ? "bulk" : "count")
|
current.isIndividual ? "individual" : (current.isBulk ? "bulk" : "count")
|
||||||
@@ -355,6 +409,9 @@ struct ProductDetailView: View {
|
|||||||
packageSize = current.packageSize.map { formatAmount($0) } ?? ""
|
packageSize = current.packageSize.map { formatAmount($0) } ?? ""
|
||||||
packageLabel = current.packageLabel ?? ""
|
packageLabel = current.packageLabel ?? ""
|
||||||
datePrecision = current.datePrecision == "month" ? "month" : "day"
|
datePrecision = current.datePrecision == "month" ? "month" : "day"
|
||||||
|
zweitBasis = current.secondaryBase ?? ""
|
||||||
|
zweitAnzahl = current.secondaryCount.map { formatAmount($0) } ?? ""
|
||||||
|
zweitMenge = current.secondaryAmount.map { formatAmount($0) } ?? ""
|
||||||
groupId = current.groupId
|
groupId = current.groupId
|
||||||
categoryId = current.categoryId
|
categoryId = current.categoryId
|
||||||
shopId = current.shopId
|
shopId = current.shopId
|
||||||
@@ -435,6 +492,17 @@ struct ProductDetailView: View {
|
|||||||
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
|
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
|
||||||
// hier bewusst NICHT mitgeschickt – sonst überschriebe das
|
// hier bewusst NICHT mitgeschickt – sonst überschriebe das
|
||||||
// Stammdaten-Speichern die dort gesetzte „Überall"-Zeile.
|
// Stammdaten-Speichern die dort gesetzte „Überall"-Zeile.
|
||||||
|
//
|
||||||
|
// Zweiteinheit nur vollstaendig; leere oder halbe Angabe loescht
|
||||||
|
// die Bruecke (secondary_base null raeumt im Backend mit auf).
|
||||||
|
let zn = Double(zweitAnzahl.replacingOccurrences(of: ",", with: "."))
|
||||||
|
let zm = Double(zweitMenge.replacingOccurrences(of: ",", with: "."))
|
||||||
|
req.sendSecondary = true
|
||||||
|
if !zweitBasis.isEmpty, let zn, let zm, zn > 0, zm > 0 {
|
||||||
|
req.secondaryBase = zweitBasis
|
||||||
|
req.secondaryCount = zn
|
||||||
|
req.secondaryAmount = zm
|
||||||
|
}
|
||||||
if current.isObject {
|
if current.isObject {
|
||||||
var fv: [String: String?] = [:]
|
var fv: [String: String?] = [:]
|
||||||
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
|
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
|
||||||
|
|||||||
Reference in New Issue
Block a user