diff --git a/incl/GS1Parser.php b/incl/GS1Parser.php new file mode 100644 index 0000000..5a79478 --- /dev/null +++ b/incl/GS1Parser.php @@ -0,0 +1,121 @@ +barcode = $barcode; + $this->parse(); + } + + private function parse() { + // Remove symbology identifier if present (e.g. ]d2 for GS1 DataMatrix) + $code = $this->barcode; + if (substr($code, 0, 3) === ']d2') { + $code = substr($code, 3); + } + + // Replace group separators (GS) with a common delimiter if needed, + // or just rely on regex. ASCII 29 is GS. + // Some scanners might map it to something else, but let's assume raw input or specific mapping. + // For now, we'll try to parse standard AIs. + + // AI 01: GTIN (14 digits) + // AI 17: Expiration Date (YYMMDD) + // AI 10: Batch/Lot (Variable length) - we might encounter this + // AI 21: Serial (Variable length) + + // Simple parsing strategy: + // 1. Look for 01 (GTIN) - fixed length 14 + // 2. Look for 17 (Exp) - fixed length 6 + + // We need to handle the stream. + $offset = 0; + $length = strlen($code); + + while ($offset < $length) { + $ai2 = substr($code, $offset, 2); + + if ($ai2 === '01') { + // GTIN: 14 digits + $this->gtin = substr($code, $offset + 2, 14); + $offset += 16; + } elseif ($ai2 === '17') { + // Expiration: 6 digits YYMMDD + $rawDate = substr($code, $offset + 2, 6); + $this->expirationDate = $this->parseDate($rawDate); + $offset += 8; + } elseif ($ai2 === '10') { + // Batch: Variable length, terminated by FNC1 or end of string + $offset += 2; + $end = $this->findNextSeparator($code, $offset); + $offset = $end; + } elseif ($ai2 === '21') { + // Serial: Variable length + $offset += 2; + $end = $this->findNextSeparator($code, $offset); + $offset = $end; + } else { + // Unknown AI or end of useful data for us. + // If we have what we need, we can stop. + // If we encounter something we don't know, we might get stuck if it's variable length. + // For now, let's just try to skip 1 char if we don't match known fixed AIs? + // No, that's dangerous. + // Let's assume standard ordering or just regex search if parsing fails. + + // Fallback: Regex search for 01 and 17 if strict parsing fails? + // Let's try to be robust. + break; + } + } + } + + private function findNextSeparator($code, $offset) { + // Check for ASCII 29 (GS) + $pos = strpos($code, chr(29), $offset); + if ($pos !== false) { + return $pos + 1; // Skip the GS + } + return strlen($code); + } + + private function parseDate($yymmdd) { + if (strlen($yymmdd) !== 6 || !is_numeric($yymmdd)) { + return null; + } + $yy = intval(substr($yymmdd, 0, 2)); + $mm = intval(substr($yymmdd, 2, 2)); + $dd = intval(substr($yymmdd, 4, 2)); + + // GS1 Date logic: + // DD = 00 means last day of month + + // Century assumption: + // GS1 General Specifications say: + // 51-99 = 1951-1999 + // 00-50 = 2000-2050 + // (This is a sliding window, usually +/- 50 years from now, but let's stick to simple logic for now) + $fullYear = ($yy >= 50 ? 1900 : 2000) + $yy; + + if ($dd === 0) { + // Last day of month + $dd = date('t', strtotime("$fullYear-$mm-01")); + } + + return sprintf("%04d-%02d-%02d", $fullYear, $mm, $dd); + } + + public function getGtin() { + return $this->gtin; + } + + public function getExpirationDate() { + return $this->expirationDate; + } + + public function isValid() { + return !empty($this->gtin); + } +} diff --git a/incl/api.inc.php b/incl/api.inc.php index 45b1c56..13f9dc8 100755 --- a/incl/api.inc.php +++ b/incl/api.inc.php @@ -356,7 +356,16 @@ public static function purchaseProduct(int $id, float $amount, string $bestbefor else $daysBestBefore = self::getDefaultBestBeforeDays($id); } - $data['best_before_date'] = self::formatBestBeforeDays($daysBestBefore); + + // Check if $daysBestBefore is a date string (YYYY-MM-DD) + if (preg_match("/^\d{4}-\d{2}-\d{2}$/", (string)$daysBestBefore)) { + $data['best_before_date'] = $daysBestBefore; + // We set this to a non-zero value to indicate success later + $daysBestBefore = 1; + } else { + $data['best_before_date'] = self::formatBestBeforeDays((int)$daysBestBefore); + } + $data_json = json_encode($data); $url = API_STOCK . "/" . $id . "/add"; diff --git a/incl/db.inc.php b/incl/db.inc.php index c8500cb..f72a9ad 100755 --- a/incl/db.inc.php +++ b/incl/db.inc.php @@ -107,6 +107,7 @@ class DatabaseConnection { "LOOKUP_USE_UPC_DATABASE" => "0", "LOOKUP_USE_OPEN_GTIN_DATABASE" => "0", "LOOKUP_USE_DISCOGS" => "0", + "GS1_PARSING_ENABLED" => "1", "LOOKUP_USE_BBUDDY_SERVER" => "0", "LOOKUP_UPC_DATABASE_KEY" => null, "LOOKUP_OPENGTIN_KEY" => null, @@ -450,8 +451,7 @@ public function setQuantityToUnknownBarcode(string $barcode, float $amount): voi * @return void */ public function insertUnrecognizedBarcode(string $barcode, float $amount = 1, string $bestBeforeInDays = null, string $price = null, ?array $productname = null): void { - if ($bestBeforeInDays == null) - $bestBeforeInDays = "NULL"; + $bestBeforeInDays = ($bestBeforeInDays === null) ? "NULL" : "'" . trim($bestBeforeInDays, "'") . "'"; if ($productname == null) { $name = "N/A"; @@ -473,8 +473,7 @@ public function insertUnrecognizedBarcode(string $barcode, float $amount = 1, st * @param null|string $price */ public function insertActionRequiredBarcode(string $barcode, ?string $bestBeforeInDays = null, ?string $price = null): void { - if ($bestBeforeInDays == null) - $bestBeforeInDays = "NULL"; + $bestBeforeInDays = ($bestBeforeInDays === null) ? "NULL" : "'" . trim($bestBeforeInDays, "'") . "'"; $this->db->exec("INSERT INTO Barcodes(barcode, name, amount, possibleMatch, requireWeight, bestBeforeInDays, price) VALUES('$barcode', 'N/A', 1, 0, 1, $bestBeforeInDays, '$price')"); diff --git a/incl/processing.inc.php b/incl/processing.inc.php index 393e174..9a80bf8 100755 --- a/incl/processing.inc.php +++ b/incl/processing.inc.php @@ -36,6 +36,22 @@ function processNewBarcode(string $barcodeInput, ?string $bestBeforeInDays = nul $config = BBConfig::getInstance(); $barcode = strtoupper($barcodeInput); + + // Check for GS1 Datamatrix + // GS1 with AI 01 must be at least 16 chars (2 for AI + 14 for GTIN) + if ($config["GS1_PARSING_ENABLED"] && (strpos($barcode, ']D2') === 0 || (strpos($barcode, '01') === 0 && strlen($barcode) >= 16))) { + // Try parsing as GS1 + require_once __DIR__ . "/GS1Parser.php"; + $parser = new GS1Parser($barcodeInput); // Use original input to preserve case/chars if needed + if ($parser->isValid()) { + $barcode = $parser->getGtin(); + $expDate = $parser->getExpirationDate(); + if ($expDate != null) { + $bestBeforeInDays = $expDate; + } + } + } + if ($barcode == $config["BARCODE_C"]) { $db->setTransactionState(STATE_CONSUME); return createLogModeChange(STATE_CONSUME); diff --git a/menu/settings.php b/menu/settings.php index 501e443..fb4f6d4 100755 --- a/menu/settings.php +++ b/menu/settings.php @@ -105,6 +105,7 @@ function getHtmlSettingsGeneral(): string { $html->addCheckbox("USE_GENERIC_NAME", "Use generic names for lookup", $config["USE_GENERIC_NAME"], false, false); $html->addCheckbox("SHOW_STOCK_ON_SCAN", "Show stock amount on scan", $config["SHOW_STOCK_ON_SCAN"], false, false); $html->addCheckbox("SAVE_BARCODE_NAME", "Save name from lookup to barcode", $config["SAVE_BARCODE_NAME"], false, false); + $html->addCheckbox("GS1_PARSING_ENABLED", "Enable GS1/Datamatrix parsing", $config["GS1_PARSING_ENABLED"], false, false); $html->addCheckbox("MORE_VERBOSE", "More verbose logs", $config["MORE_VERBOSE"], false, false); $html->addLineBreak(2); $html->addHtml('Hint: You can find picture files of the default barcodes in the "example" folder or online');