MHD per Texterkennung einlesen
In der Chargenzeile beim Einlagern gibt es einen Knopf "MHD scannen". Er oeffnet die Texterkennung von VisionKit; erkannte Daten erscheinen als Vorschlag und werden erst uebernommen, wenn man sie antippt. Bewusst kein automatisches Uebernehmen: Eine Fehllesung beim MHD faellt sonst erst auf, wenn die Ware bereits falsch einsortiert ist. Der Parser deckt die Schreibweisen ab, die auf Verpackungen vorkommen - Tagesdaten (12.09.2026, 2026-09-12, 12/09/2026, 12.09.26) und Monatsangaben (09/2026, 09.2026, 2026-09, 09/26). Erkennt der Aufdruck nur Monat und Jahr, stellt sich die Eingabe sichtbar auf Monat/Jahr um. Damit aus Chargennummern und EAN-Codes keine Phantasiedaten werden, greifen mehrere Einschraenkungen: Muster mit Tag werden zuerst geprueft, ein bereits erkannter Textabschnitt wird nicht erneut ausgewertet, Monate ausserhalb 1-12 entfallen und das Jahr muss im Fenster von fuenf Jahren rueckwaerts bis dreissig vorwaerts liegen. Der Knopf erscheint nur, wenn das Geraet Texterkennung unterstuetzt (Neural Engine); sonst bleibt die Eingabe von Hand. Getestet: Der Parser wurde eigenstaendig uebersetzt und gegen 14 Faelle geprueft, alle korrekt - darunter, dass "12.09.2026" nicht als Monatsangabe gelesen wird, dass eine EAN keine Treffer erzeugt und dass unplausible Jahre und Monate verworfen werden. Der iOS-Geraetebuild laeuft fehlerfrei durch. Die Erkennung an einer echten Verpackung habe ich nicht selbst ausprobiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,27 @@ actor APIClient {
|
|||||||
return try await send(request, as: Product.self)
|
return try await send(request, as: Product.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateProduct(id: Int, _ payload: ProductUpdateRequest) async throws -> Product {
|
||||||
|
var request = try makeRequest("/products/\(id)", method: "PATCH")
|
||||||
|
try jsonBody(&request, payload)
|
||||||
|
return try await send(request, as: Product.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Listen
|
||||||
|
|
||||||
|
func shoppingList() async throws -> [ShoppingItem] {
|
||||||
|
try await send(try makeRequest("/shopping-list"), as: [ShoppingItem].self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shoppingGroups() async throws -> [GroupShoppingItem] {
|
||||||
|
try await send(try makeRequest("/shopping-list/groups"), as: [GroupShoppingItem].self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expiring(days: Int? = nil) async throws -> [ExpiringItem] {
|
||||||
|
let path = days.map { "/expiring?days=\($0)" } ?? "/expiring"
|
||||||
|
return try await send(try makeRequest(path), as: [ExpiringItem].self)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Bestand
|
// MARK: - Bestand
|
||||||
|
|
||||||
func lots(productId: Int) async throws -> [Lot] {
|
func lots(productId: Int) async throws -> [Lot] {
|
||||||
@@ -124,4 +145,16 @@ actor APIClient {
|
|||||||
try jsonBody(&request, payload)
|
try jsonBody(&request, payload)
|
||||||
return try await send(request, as: StockResponse.self)
|
return try await send(request, as: StockResponse.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLot(id: Int, _ payload: LotUpdateRequest) async throws -> Lot {
|
||||||
|
var request = try makeRequest("/lots/\(id)", method: "PATCH")
|
||||||
|
try jsonBody(&request, payload)
|
||||||
|
return try await send(request, as: Lot.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteLot(id: Int) async throws {
|
||||||
|
let request = try makeRequest("/lots/\(id)", method: "DELETE")
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
try check(response, data: data) // 204: kein Rumpf zum Auswerten
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
ios/Sources/BestBeforeText.swift
Normal file
94
ios/Sources/BestBeforeText.swift
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Erkennt Mindesthaltbarkeitsdaten in Text, wie er auf Verpackungen steht.
|
||||||
|
///
|
||||||
|
/// Aufdrucke sind uneinheitlich: mal "12.09.2026", mal "09/26", mal
|
||||||
|
/// "mind. haltbar bis: 2026-09-12". Gefundene Treffer werden dem Benutzer zur
|
||||||
|
/// Auswahl vorgelegt und nie ungefragt uebernommen – eine Fehllesung beim MHD
|
||||||
|
/// faellt sonst erst auf, wenn die Ware schon falsch einsortiert ist.
|
||||||
|
enum BestBeforeText {
|
||||||
|
|
||||||
|
struct Match: Identifiable, Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
let date: Date
|
||||||
|
/// "day" oder "month" – ein Aufdruck "09/2026" nennt keinen Tag.
|
||||||
|
let precision: String
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "de_DE")
|
||||||
|
formatter.dateFormat = precision == "month" ? "MM/yyyy" : "dd.MM.yyyy"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reihenfolge ist wichtig: zuerst die Muster mit Tag, damit "12.09.2026"
|
||||||
|
// nicht als Monatsangabe "12.09" missverstanden wird.
|
||||||
|
private static let patterns: [(regex: String, order: [Character], precision: String)] = [
|
||||||
|
(#"(\d{1,2})[./-](\d{1,2})[./-](\d{4})"#, ["d", "m", "y"], "day"),
|
||||||
|
(#"(\d{4})[./-](\d{1,2})[./-](\d{1,2})"#, ["y", "m", "d"], "day"),
|
||||||
|
(#"(\d{1,2})[./-](\d{1,2})[./-](\d{2})(?!\d)"#, ["d", "m", "Y"], "day"),
|
||||||
|
(#"(\d{1,2})[./-](\d{4})"#, ["m", "y"], "month"),
|
||||||
|
(#"(\d{4})[./-](\d{1,2})(?!\d)"#, ["y", "m"], "month"),
|
||||||
|
(#"(\d{1,2})[./-](\d{2})(?!\d)"#, ["m", "Y"], "month"),
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Alle plausiblen Daten aus einem Textstueck, in Fundreihenfolge und ohne Dubletten.
|
||||||
|
static func matches(in text: String) -> [Match] {
|
||||||
|
var found: [Match] = []
|
||||||
|
var belegt: [Range<String.Index>] = []
|
||||||
|
|
||||||
|
for pattern in patterns {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern.regex) else { continue }
|
||||||
|
let range = NSRange(text.startIndex..., in: text)
|
||||||
|
|
||||||
|
for result in regex.matches(in: text, range: range) {
|
||||||
|
guard let treffer = Range(result.range, in: text) else { continue }
|
||||||
|
// Ein Textabschnitt zaehlt nur einmal: sonst liest das Monatsmuster
|
||||||
|
// aus einem bereits erkannten Tagesdatum noch einen zweiten Treffer.
|
||||||
|
if belegt.contains(where: { $0.overlaps(treffer) }) { continue }
|
||||||
|
|
||||||
|
var teile: [Character: Int] = [:]
|
||||||
|
for (index, key) in pattern.order.enumerated() {
|
||||||
|
guard let teil = Range(result.range(at: index + 1), in: text),
|
||||||
|
let zahl = Int(text[teil]) else { continue }
|
||||||
|
teile[key] = zahl
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let datum = build(teile, precision: pattern.precision) else { continue }
|
||||||
|
belegt.append(treffer)
|
||||||
|
found.append(Match(date: datum, precision: pattern.precision))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func build(_ teile: [Character: Int], precision: String) -> Date? {
|
||||||
|
let monat = teile["m"] ?? 0
|
||||||
|
guard (1...12).contains(monat) else { return nil }
|
||||||
|
|
||||||
|
// "Y" ist ein zweistelliges Jahr: 26 -> 2026.
|
||||||
|
let jahr: Int
|
||||||
|
if let vier = teile["y"] {
|
||||||
|
jahr = vier
|
||||||
|
} else if let zwei = teile["Y"] {
|
||||||
|
jahr = 2000 + zwei
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Aufdrucke liegen realistisch in diesem Fenster; alles andere ist
|
||||||
|
// vermutlich eine Chargennummer und kein MHD.
|
||||||
|
let aktuell = Calendar.current.component(.year, from: Date())
|
||||||
|
guard (aktuell - 5)...(aktuell + 30) ~= jahr else { return nil }
|
||||||
|
|
||||||
|
var komponenten = DateComponents()
|
||||||
|
komponenten.year = jahr
|
||||||
|
komponenten.month = monat
|
||||||
|
komponenten.day = precision == "month" ? 1 : (teile["d"] ?? 1)
|
||||||
|
|
||||||
|
if precision == "day" {
|
||||||
|
guard let tag = teile["d"], (1...31).contains(tag) else { return nil }
|
||||||
|
}
|
||||||
|
return Calendar.current.date(from: komponenten)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Damit sich eine UUID direkt als sheet(item:) verwenden laesst.
|
||||||
|
extension UUID: Identifiable {
|
||||||
|
public var id: UUID { self }
|
||||||
|
}
|
||||||
|
|
||||||
extension Date {
|
extension Date {
|
||||||
/// Erster Tag des Monats - so wird eine Monatsangabe an den Server geschickt.
|
/// Erster Tag des Monats - so wird eine Monatsangabe an den Server geschickt.
|
||||||
var startOfMonth: Date {
|
var startOfMonth: Date {
|
||||||
@@ -77,6 +82,8 @@ struct CheckInFormView: View {
|
|||||||
/// Gilt fuer den ganzen Vorgang: Auf einer Packung steht entweder ein
|
/// Gilt fuer den ganzen Vorgang: Auf einer Packung steht entweder ein
|
||||||
/// Tagesdatum oder nur Monat/Jahr - nicht beides gemischt.
|
/// Tagesdatum oder nur Monat/Jahr - nicht beides gemischt.
|
||||||
@State private var precision: String = "day"
|
@State private var precision: String = "day"
|
||||||
|
/// Charge, fuer die gerade das MHD abgescannt wird.
|
||||||
|
@State private var scanLineId: UUID?
|
||||||
@State private var unit: String = ""
|
@State private var unit: String = ""
|
||||||
@State private var locationId: Int?
|
@State private var locationId: Int?
|
||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@@ -156,6 +163,15 @@ struct CheckInFormView: View {
|
|||||||
} else {
|
} else {
|
||||||
DatePicker("MHD", selection: $line.bestBefore, displayedComponents: .date)
|
DatePicker("MHD", selection: $line.bestBefore, displayedComponents: .date)
|
||||||
}
|
}
|
||||||
|
if DateScanView.isSupported {
|
||||||
|
Button {
|
||||||
|
scanLineId = line.id
|
||||||
|
} label: {
|
||||||
|
Label("MHD scannen", systemImage: "text.viewfinder")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
@@ -181,6 +197,19 @@ struct CheckInFormView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||||||
}
|
}
|
||||||
|
.sheet(item: $scanLineId) { id in
|
||||||
|
NavigationStack {
|
||||||
|
DateScanView { datum, erkanntePraezision in
|
||||||
|
// Nennt der Aufdruck keinen Tag, stellt sich die Eingabe
|
||||||
|
// sichtbar auf Monat/Jahr um.
|
||||||
|
if erkanntePraezision == "month" { precision = "month" }
|
||||||
|
if let index = lines.firstIndex(where: { $0.id == id }) {
|
||||||
|
lines[index].bestBefore = datum
|
||||||
|
lines[index].hasDate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Gibt es ein Gebinde, ist das die naheliegende Eingabe.
|
// Gibt es ein Gebinde, ist das die naheliegende Eingabe.
|
||||||
unit = packageOptionName ?? product.unitName
|
unit = packageOptionName ?? product.unitName
|
||||||
|
|||||||
136
ios/Sources/DateScanView.swift
Normal file
136
ios/Sources/DateScanView.swift
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import VisionKit
|
||||||
|
|
||||||
|
/// Liest das MHD per Texterkennung von der Verpackung ab.
|
||||||
|
///
|
||||||
|
/// Gefundene Daten werden als Vorschlag angezeigt; uebernommen wird erst,
|
||||||
|
/// was der Benutzer antippt.
|
||||||
|
struct DateScanView: View {
|
||||||
|
var onPick: (Date, String) -> Void
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var matches: [BestBeforeText.Match] = []
|
||||||
|
|
||||||
|
/// Texterkennung braucht Geraete mit Neural Engine; auf aelteren fehlt sie.
|
||||||
|
static var isSupported: Bool {
|
||||||
|
DataScannerViewController.isSupported && DataScannerViewController.isAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
if Self.isSupported {
|
||||||
|
TextScannerRepresentable { text in
|
||||||
|
let neu = BestBeforeText.matches(in: text)
|
||||||
|
guard !neu.isEmpty else { return }
|
||||||
|
// Neueste Funde vorn, hoechstens eine Handvoll anzeigen.
|
||||||
|
for treffer in neu where !matches.contains(where: {
|
||||||
|
$0.date == treffer.date && $0.precision == treffer.precision
|
||||||
|
}) {
|
||||||
|
matches.insert(treffer, at: 0)
|
||||||
|
}
|
||||||
|
if matches.count > 5 { matches = Array(matches.prefix(5)) }
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
} else {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
Text("Dieses Gerät unterstützt keine Texterkennung.")
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
if matches.isEmpty {
|
||||||
|
hinweis("Das Mindesthaltbarkeitsdatum in den Sucher halten.")
|
||||||
|
} else {
|
||||||
|
ForEach(matches) { treffer in
|
||||||
|
Button {
|
||||||
|
onPick(treffer.date, treffer.precision)
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
Text(treffer.label).font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Text("Übernehmen").font(.subheadline)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(.thinMaterial)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
hinweis("Bitte gegen den Aufdruck prüfen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationTitle("MHD scannen")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hinweis(_ text: String) -> some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.caption)
|
||||||
|
.padding(10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Duenne Huelle um den Text-Scanner von VisionKit.
|
||||||
|
private struct TextScannerRepresentable: UIViewControllerRepresentable {
|
||||||
|
var onText: (String) -> Void
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator { Coordinator(onText: onText) }
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> DataScannerViewController {
|
||||||
|
let controller = DataScannerViewController(
|
||||||
|
recognizedDataTypes: [.text()],
|
||||||
|
qualityLevel: .accurate,
|
||||||
|
recognizesMultipleItems: true,
|
||||||
|
isHighFrameRateTrackingEnabled: false,
|
||||||
|
isHighlightingEnabled: true
|
||||||
|
)
|
||||||
|
controller.delegate = context.coordinator
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ controller: DataScannerViewController, context: Context) {
|
||||||
|
guard !controller.isScanning else { return }
|
||||||
|
try? controller.startScanning()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dismantleUIViewController(_ controller: DataScannerViewController, coordinator: Coordinator) {
|
||||||
|
controller.stopScanning()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
|
||||||
|
let onText: (String) -> Void
|
||||||
|
init(onText: @escaping (String) -> Void) { self.onText = onText }
|
||||||
|
|
||||||
|
func dataScanner(_ scanner: DataScannerViewController,
|
||||||
|
didAdd addedItems: [RecognizedItem],
|
||||||
|
allItems: [RecognizedItem]) {
|
||||||
|
handle(allItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataScanner(_ scanner: DataScannerViewController,
|
||||||
|
didUpdate updatedItems: [RecognizedItem],
|
||||||
|
allItems: [RecognizedItem]) {
|
||||||
|
handle(allItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle(_ items: [RecognizedItem]) {
|
||||||
|
let text = items.compactMap { item -> String? in
|
||||||
|
if case .text(let erkannt) = item { return erkannt.transcript }
|
||||||
|
return nil
|
||||||
|
}.joined(separator: " ")
|
||||||
|
guard !text.isEmpty else { return }
|
||||||
|
onText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -211,3 +211,109 @@ struct NewProductRequest: Codable {
|
|||||||
case groupId = "group_id"
|
case groupId = "group_id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Listen (Einkaufsliste, bald ablaufend)
|
||||||
|
|
||||||
|
struct ShoppingItem: Codable, Identifiable {
|
||||||
|
let productId: Int
|
||||||
|
let name: String
|
||||||
|
let baseUnit: String
|
||||||
|
let packageSize: Double?
|
||||||
|
let stock: Double
|
||||||
|
let minStock: Double
|
||||||
|
let deficit: Double
|
||||||
|
|
||||||
|
var id: Int { productId }
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name, stock, deficit
|
||||||
|
case productId = "product_id"
|
||||||
|
case baseUnit = "base_unit"
|
||||||
|
case packageSize = "package_size"
|
||||||
|
case minStock = "min_stock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GroupShoppingItem: Codable, Identifiable {
|
||||||
|
let groupId: Int
|
||||||
|
let name: String
|
||||||
|
let stock: Double
|
||||||
|
let minStock: Double
|
||||||
|
let deficit: Double
|
||||||
|
let unitName: String
|
||||||
|
let productCount: Int
|
||||||
|
|
||||||
|
var id: Int { groupId }
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name, stock, deficit
|
||||||
|
case groupId = "group_id"
|
||||||
|
case minStock = "min_stock"
|
||||||
|
case unitName = "unit_name"
|
||||||
|
case productCount = "product_count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExpiringItem: Codable, Identifiable {
|
||||||
|
let lotId: Int
|
||||||
|
let productId: Int
|
||||||
|
let productName: String
|
||||||
|
let quantity: Double
|
||||||
|
let baseUnit: String
|
||||||
|
let bestBefore: String
|
||||||
|
let bestBeforePrecision: String?
|
||||||
|
let daysLeft: Int
|
||||||
|
let packageSize: Double?
|
||||||
|
let packageLabel: String?
|
||||||
|
let unitName: String
|
||||||
|
let unitFactor: Double
|
||||||
|
|
||||||
|
var id: Int { lotId }
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case quantity
|
||||||
|
case lotId = "lot_id"
|
||||||
|
case productId = "product_id"
|
||||||
|
case productName = "product_name"
|
||||||
|
case baseUnit = "base_unit"
|
||||||
|
case bestBefore = "best_before"
|
||||||
|
case bestBeforePrecision = "best_before_precision"
|
||||||
|
case daysLeft = "days_left"
|
||||||
|
case packageSize = "package_size"
|
||||||
|
case packageLabel = "package_label"
|
||||||
|
case unitName = "unit_name"
|
||||||
|
case unitFactor = "unit_factor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Aenderungen
|
||||||
|
|
||||||
|
/// Nur gesetzte Felder werden geschickt (PATCH).
|
||||||
|
struct ProductUpdateRequest: Codable {
|
||||||
|
var name: String?
|
||||||
|
var brand: String?
|
||||||
|
var packageSize: Double?
|
||||||
|
var packageLabel: String?
|
||||||
|
var datePrecision: String?
|
||||||
|
var minStock: Double?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name, brand
|
||||||
|
case packageSize = "package_size"
|
||||||
|
case packageLabel = "package_label"
|
||||||
|
case datePrecision = "date_precision"
|
||||||
|
case minStock = "min_stock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LotUpdateRequest: Codable {
|
||||||
|
var quantity: Double?
|
||||||
|
var bestBefore: String?
|
||||||
|
var bestBeforePrecision: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case quantity
|
||||||
|
case bestBefore = "best_before"
|
||||||
|
case bestBeforePrecision = "best_before_precision"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user