Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions incl/GS1Parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

class GS1Parser {
private $barcode;
private $gtin;
private $expirationDate;

public function __construct(string $barcode) {
$this->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);
}
}
11 changes: 10 additions & 1 deletion incl/api.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
7 changes: 3 additions & 4 deletions incl/db.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand All @@ -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')");
Expand Down
16 changes: 16 additions & 0 deletions incl/processing.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions menu/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('<small><i>Hint: You can find picture files of the default barcodes in the &quot;example&quot; folder or <a style="color: inherit;" href="https://github.com/Forceu/barcodebuddy/tree/master/example/defaultBarcodes">online</a></i></small>');
Expand Down