diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php
index 0ebe85f6..68c8fe76 100644
--- a/controllers/StockApiController.php
+++ b/controllers/StockApiController.php
@@ -23,6 +23,18 @@ class StockApiController extends BaseApiController
}
}
+ public function ProductBarcodeDetails(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
+ {
+ try
+ {
+ return $this->ApiResponse($response, $this->getDatabase()->product_barcodes()->where('barcode = :1', $args['barcode'])->fetch());
+ }
+ catch (\Exception $ex)
+ {
+ return $this->GenericErrorResponse($response, $ex->getMessage());
+ }
+ }
+
public function ProductDetailsByBarcode(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
diff --git a/controllers/StockController.php b/controllers/StockController.php
index 693bd016..3971f895 100644
--- a/controllers/StockController.php
+++ b/controllers/StockController.php
@@ -179,6 +179,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'productform', [
'locations' => $this->getDatabase()->locations()->orderBy('name'),
+ 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'),
@@ -195,6 +196,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'productform', [
'product' => $product,
'locations' => $this->getDatabase()->locations()->orderBy('name'),
+ 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'),
@@ -354,6 +356,34 @@ class StockController extends BaseController
]);
}
+ public function ProductBarcodesEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
+ {
+ $product = null;
+ if (isset($request->getQueryParams()['product']))
+ {
+ $product = $this->getDatabase()->products($request->getQueryParams()['product']);
+ }
+
+ if ($args['productBarcodeId'] == 'new')
+ {
+ return $this->renderPage($response, 'productbarcodesform', [
+ 'mode' => 'create',
+ 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
+ 'product' => $product,
+ 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
+ ]);
+ }
+ else
+ {
+ return $this->renderPage($response, 'productbarcodesform', [
+ 'mode' => 'edit',
+ 'barcode' => $this->getDatabase()->product_barcodes($args['productBarcodeId']),
+ 'product' => $product,
+ 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
+ ]);
+ }
+ }
+
public function QuantityUnitConversionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$product = null;
diff --git a/grocy.openapi.json b/grocy.openapi.json
index c19a0c6b..9ade80cf 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -2703,6 +2703,57 @@
}
}
},
+ "/productbarcodedetails/{barcode}": {
+ "get": {
+ "summary": "Executes a product barcode details lookoup via the configured plugin with the given barcode",
+ "tags": [
+ "Product"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "barcode",
+ "required": true,
+ "description": "The barcode to lookup up",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "add",
+ "required": false,
+ "description": "When true, the product is added to the database on a successful lookup and the new product id is in included in the response",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "An ProductBarcodeDetails object or null, when nothing was found for the given barcode",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProductBarcodeDetailsResponse"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "The operation was not successful (possible errors are: Plugin error)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GenericErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": {
"post": {
"summary": "Adds all missing products for the given recipe to the shopping list",
@@ -3374,6 +3425,7 @@
"enum": [
"products",
"chores",
+ "product_barcodes",
"batteries",
"locations",
"quantity_units",
@@ -3862,6 +3914,28 @@
}
}
},
+ "ProductBarcodeDetailsResponse": {
+ "type": "object",
+ "properties": {
+ "product_id": {
+ "type": "integer"
+ },
+ "barcode": {
+ "type": "string"
+ },
+ "qu_factor_purchase_to_stock": {
+ "type": "number",
+ "format": "number"
+ },
+ "barcode": {
+ "type": "string",
+ "description": "Can contain multiple barcodes separated by comma"
+ },
+ "shopping_location_id": {
+ "type": "integer"
+ }
+ }
+ },
"ExternalBarcodeLookupResponse": {
"type": "object",
"properties": {
diff --git a/migrations/0103.sql b/migrations/0103.sql
new file mode 100644
index 00000000..2ee0b9d7
--- /dev/null
+++ b/migrations/0103.sql
@@ -0,0 +1,19 @@
+ALTER TABLE stock_log
+ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
+
+ALTER TABLE stock
+ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
+
+UPDATE stock
+ SET qu_factor_purchase_to_stock = (select qu_factor_purchase_to_stock from products where product_id = id);
+
+UPDATE stock_log
+ SET qu_factor_purchase_to_stock = (select qu_factor_purchase_to_stock from products where product_id = id);
+
+CREATE TABLE product_barcodes (
+id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
+product_id INT NOT NULL,
+barcode TEXT NOT NULL,
+qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1,
+shopping_location_id INTEGER
+);
diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js
index 9b6c3ba4..3f9a81c3 100644
--- a/public/viewjs/components/productcard.js
+++ b/public/viewjs/components/productcard.js
@@ -80,7 +80,14 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null)
{
- $('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
+ if (productDetails.last_qu_factor_purchase_to_stock > 1)
+ {
+ $('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per 1 ' + productDetails.quantity_unit_purchase.name + ' of ' + productDetails.last_qu_factor_purchase_to_stock + ' ' + productDetails.quantity_unit_stock.name_plural);
+ }
+ else
+ {
+ $('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
+ }
}
else
{
diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js
index 15649c85..abbd5589 100644
--- a/public/viewjs/components/productpicker.js
+++ b/public/viewjs/components/productpicker.js
@@ -132,6 +132,7 @@ $('#product_id_text_input').on('blur', function(e)
{
return;
}
+ $('#product_id').attr("barcode", "null");
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
@@ -139,6 +140,7 @@ $('#product_id_text_input').on('blur', function(e)
if (GetUriParam('addbarcodetoselection') === undefined && input.length > 0 && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
+ $('#product_id').attr("barcode", input);
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js
index b1dedbc7..9570cda5 100644
--- a/public/viewjs/mealplan.js
+++ b/public/viewjs/mealplan.js
@@ -180,6 +180,11 @@ var calendar = $("#calendar").fullCalendar({
productDetails.last_price = 0;
}
+ if (productDetails.last_qu_factor_purchase_to_stock === null)
+ {
+ productDetails.last_qu_factor_purchase_to_stock = 1;
+ }
+
element.attr("data-product-details", event.productDetails);
var productOrderMissingButtonDisabledClasses = "disabled";
@@ -205,7 +210,7 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
- costsAndCaloriesPerServing = '
' + productDetails.last_price / productDetails.product.qu_factor_purchase_to_stock * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '';
+ costsAndCaloriesPerServing = '' + productDetails.last_price / productDetails.last_qu_factor_purchase_to_stock * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '';
}
else
{
diff --git a/public/viewjs/productbarcodesform.js b/public/viewjs/productbarcodesform.js
new file mode 100644
index 00000000..58617402
--- /dev/null
+++ b/public/viewjs/productbarcodesform.js
@@ -0,0 +1,98 @@
+$('#save-barcode-button').on('click', function(e)
+{
+ e.preventDefault();
+
+ var jsonData = $('#barcode-form').serializeJSON();
+ Grocy.FrontendHelpers.BeginUiBusy("barcode-form");
+
+ if (Grocy.EditMode === 'create')
+ {
+ Grocy.Api.Post('objects/product_barcodes', jsonData,
+ function(result)
+ {
+ Grocy.EditObjectId = result.created_object_id;
+ window.location.href = U("/product/" + GetUriParam("product"));
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("barcode-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
+ }
+ );
+ }
+ else
+ {
+ Grocy.Api.Put('objects/product_barcodes/' + Grocy.EditObjectId, jsonData,
+ function(result)
+ {
+ window.location.href = U("/product/" + GetUriParam("product"));
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("barcode-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
+ }
+ );
+ }
+
+ Grocy.Api.Get('stock/products/' + jsonData.product_id,
+ function(productDetails)
+ {
+ var existingBarcodes = productDetails.product.barcode || '';
+ if (existingBarcodes.length === 0)
+ {
+ productDetails.product.barcode = jsonData.barcode;
+ }
+ else
+ {
+ productDetails.product.barcode += ',' + jsonData.barcode;
+ }
+ var jsonDataProduct = {};
+ jsonDataProduct.barcode = productDetails.product.barcode;
+
+ Grocy.Api.Put('objects/products/' + jsonData.product_id, jsonDataProduct,
+ function(result)
+ {
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("barcode-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
+ }
+ );
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("barcode-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
+ }
+ );
+});
+
+$('#barcode').on('change', function(e)
+{
+ Grocy.FrontendHelpers.ValidateForm('barcode-form');
+});
+
+$('#qu_factor_purchase_to_stock').on('change', function(e)
+{
+ Grocy.FrontendHelpers.ValidateForm('barcode-form');
+});
+
+$('#barcode-form input').keydown(function(event)
+{
+ if (event.keyCode === 13) //Enter
+ {
+ event.preventDefault();
+
+ if (document.getElementById('barcode-form').checkValidity() === false) //There is at least one validation error
+ {
+ return false;
+ }
+ else
+ {
+ $('#save-barcode-button').click();
+ }
+ }
+});
+Grocy.FrontendHelpers.ValidateForm('barcode-form');
diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js
index 4e4edaa6..f5c55ab5 100644
--- a/public/viewjs/productform.js
+++ b/public/viewjs/productform.js
@@ -249,12 +249,28 @@ $('#product-form input').keyup(function(event)
{
$("#qu-conversion-add-button").removeClass("disabled");
}
+ if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
+ {
+ $("#barcode-add-button").addClass("disabled");
+ }
+ else
+ {
+ $("#barcode-add-button").removeClass("disabled");
+ }
});
$('#product-form select').change(function(event)
{
Grocy.FrontendHelpers.ValidateForm('product-form');
+ if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
+ {
+ $("#barcode-add-button").addClass("disabled");
+ }
+ else
+ {
+ $("#barcode-add-button").removeClass("disabled");
+ }
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#qu-conversion-add-button").addClass("disabled");
@@ -365,6 +381,18 @@ var quConversionsTable = $('#qu-conversions-table').DataTable({
$('#qu-conversions-table tbody').removeClass("d-none");
quConversionsTable.columns.adjust().draw();
+var barcodeTable = $('#barcode-table').DataTable({
+ 'order': [[1, 'asc']],
+ "orderFixed": [[1, 'asc']],
+ 'columnDefs': [
+ { 'orderable': false, 'targets': 0 },
+ { 'searchable': false, "targets": 0 }
+ ]
+});
+$('#barcode-table tbody').removeClass("d-none");
+barcodeTable.columns.adjust().draw();
+
+
Grocy.Components.UserfieldsForm.Load();
$("#name").trigger("keyup");
$('#name').focus();
@@ -407,6 +435,29 @@ $(document).on('click', '.qu-conversion-delete-button', function(e)
console.error(xhr);
}
);
+
+ var newBarcode = '';
+ productBarcode.split(',').forEach(function(item)
+ {
+ if(barcode != item)
+ {
+ newBarcode += ',' + item;
+ }
+ });
+
+ var jsonDataProduct = {};
+ jsonDataProduct.barcode = newBarcode;
+
+ Grocy.Api.Put('objects/products/' + productId, jsonDataProduct,
+ function(result)
+ {
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("product-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
+ }
+ );
}
}
});
@@ -425,6 +476,82 @@ $("#qu-conversion-add-button").on("click", function(e)
$('#save-product-button').click();
});
+$(document).on('click', '.barcode-delete-button', function(e)
+{
+ var objectId = $(e.currentTarget).attr('data-barcode-id');
+ var productId = $(e.currentTarget).attr('data-product-id');
+ var barcode = $(e.currentTarget).attr('data-barcode');
+ var productBarcode = $(e.currentTarget).attr('data-product-barcode');
+
+ bootbox.confirm({
+ message: __t('Are you sure to remove this barcode?'),
+ closeButton: false,
+ buttons: {
+ confirm: {
+ label: __t('Yes'),
+ className: 'btn-success'
+ },
+ cancel: {
+ label: __t('No'),
+ className: 'btn-danger'
+ }
+ },
+ callback: function(result)
+ {
+ if (result === true)
+ {
+ Grocy.Api.Delete('objects/product_barcodes/' + objectId, { },
+ function(result)
+ {
+ Grocy.ProductEditFormRedirectUri = "reload";
+ $('#save-product-button').click();
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+
+ var newBarcode = '';
+ productBarcode.split(',').forEach(function(item)
+ {
+ if(barcode != item)
+ {
+ newBarcode += ',' + item;
+ }
+ });
+
+ var jsonDataProduct = {};
+ jsonDataProduct.barcode = newBarcode;
+
+ Grocy.Api.Put('objects/products/' + productId, jsonDataProduct,
+ function(result)
+ {
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy("product-form");
+ Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
+ }
+ );
+ }
+ }
+ });
+});
+
+$(document).on('click', '.barcode-edit-button', function (e)
+{
+ var id = $(e.currentTarget).attr('data-barcode-id');
+ Grocy.ProductEditFormRedirectUri = U("/productbarcodes/" + id.toString() + "?product=editobjectid");
+ $('#save-product-button').click();
+});
+
+$("#barcode-add-button").on("click", function(e)
+{
+ Grocy.ProductEditFormRedirectUri = U("/productbarcodes/new?product=editobjectid");
+ $('#save-product-button').click();
+});
+
$('#qu_id_purchase').blur(function(e)
{
// Preset the stock quantity unit with the purchase quantity unit, if the stock quantity unit is unset.
diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js
index d82dbb6c..224c6497 100644
--- a/public/viewjs/purchase.js
+++ b/public/viewjs/purchase.js
@@ -141,13 +141,66 @@ if (Grocy.Components.ProductPicker !== undefined)
{
Grocy.Components.ProductCard.Refresh(productId);
+ if (document.getElementById("product_id").getAttribute("barcode") != "null")
+ {
+ Grocy.Api.Get('productbarcodedetails/' + document.getElementById("product_id").getAttribute("barcode"),
+ function(resultBarcode)
+ {
+ if (resultBarcode != null)
+ {
+ $('#product_id').attr("barcode-qu-factor-purchase-to-stock", resultBarcode.qu_factor_purchase_to_stock);
+ $('#product_id').attr("barcode-shopping-location-id", resultBarcode.shopping_location_id);
+ }
+ else
+ {
+ $('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
+ $('#product_id').attr("barcode-shopping-location-id", "null");
+ }
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+ }
+ else
+ {
+ $('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
+ $('#product_id').attr("barcode-shopping-location-id", "null");
+ }
+
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
{
- $('#price').val(parseFloat(productDetails.last_price).toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 }));
+ $('#price').val(parseFloat(productDetails.last_price).toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 }));
+
+ var qu_factor_purchase_to_stock = null;
+ var barcode_shopping_location_id = null;
+
+ if (document.getElementById("product_id").getAttribute("barcode") != "null" && document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock") != "null")
+ {
+ qu_factor_purchase_to_stock = document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock");
+ barcode_shopping_location_id = document.getElementById("product_id").getAttribute("barcode-shopping-location-id");
+ }
+ else
+ {
+ if (productDetails.last_qu_factor_purchase_to_stock != null)
+ {
+ qu_factor_purchase_to_stock = productDetails.last_qu_factor_purchase_to_stock;
+ }
+ else
+ {
+ qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
+ }
+ }
+
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
- if (productDetails.last_shopping_location_id != null)
+ if (barcode_shopping_location_id != null)
+ {
+ Grocy.Components.ShoppingLocationPicker.SetId(barcode_shopping_location_id);
+ }
+ else if (productDetails.last_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
}
@@ -162,20 +215,21 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
- $('#amount_qu_unit').attr("qu-factor-purchase-to-stock", productDetails.product.qu_factor_purchase_to_stock);
+ $('#amount_qu_unit').attr("qu-factor-purchase-to-stock", qu_factor_purchase_to_stock);
$('#amount_qu_unit').attr("quantity-unit-purchase-name", productDetails.quantity_unit_purchase.name);
$('#amount_qu_unit').attr("quantity-unit-stock-name", productDetails.quantity_unit_stock.name);
$('#amount_qu_unit').attr("quantity-unit-stock-name-plural", productDetails.quantity_unit_stock.name_plural);
- $('#qu_factor_purchase_to_stock').val(productDetails.product.qu_factor_purchase_to_stock);
+ $('#qu_factor_purchase_to_stock').val(qu_factor_purchase_to_stock);
- if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock)
+
+ if (qu_factor_purchase_to_stock == 1)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
$('#group-qu_factor_purchase_to_stock').addClass('d-none');
}
else
{
- $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(productDetails.product.qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
+ $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
$('#group-qu_factor_purchase_to_stock').removeClass('d-none');
}
@@ -200,7 +254,7 @@ if (Grocy.Components.ProductPicker !== undefined)
if (productDetails.product.enable_tare_weight_handling == 1)
{
- var minAmount = parseFloat(productDetails.product.tare_weight) / productDetails.product.qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
+ var minAmount = parseFloat(productDetails.product.tare_weight) / qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
$("#amount").attr("min", minAmount);
$("#amount").attr("step", "0.0001");
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString()));
diff --git a/routes.php b/routes.php
index c432ecee..4da065c0 100644
--- a/routes.php
+++ b/routes.php
@@ -45,6 +45,7 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/stockentry/{entryId}', '\Grocy\Controllers\StockController:StockEntryEditForm');
$group->get('/products', '\Grocy\Controllers\StockController:ProductsList');
$group->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
+ $group->get('/productbarcodes/{productBarcodeId}', '\Grocy\Controllers\StockController:ProductBarcodesEditForm');
$group->get('/stocksettings', '\Grocy\Controllers\StockController:StockSettings');
$group->get('/locations', '\Grocy\Controllers\StockController:LocationsList');
$group->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
@@ -199,6 +200,7 @@ $app->group('/api', function(RouteCollectorProxy $group)
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
$group->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
+ $group->get('/productbarcodedetails/{barcode}', '\Grocy\Controllers\StockApiController:ProductBarcodeDetails');
}
// Shopping list
diff --git a/services/StockService.php b/services/StockService.php
index 5c6e0a14..e9eb6aa9 100644
--- a/services/StockService.php
+++ b/services/StockService.php
@@ -127,12 +127,14 @@ class StockService extends BaseService
$averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days);
$lastPrice = null;
+ $lastQuFactorPurchaseToStock = null;
$defaultShoppingLocation = null;
$lastShoppingLocation = null;
$lastLogRow = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch();
if ($lastLogRow !== null && !empty($lastLogRow))
{
$lastPrice = $lastLogRow->price;
+ $lastQuFactorPurchaseToStock = $lastLogRow->qu_factor_purchase_to_stock;
$lastShoppingLocation = $lastLogRow->shopping_location_id;
}
@@ -155,6 +157,7 @@ class StockService extends BaseService
'quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
+ 'last_qu_factor_purchase_to_stock' => $lastQuFactorPurchaseToStock,
'last_shopping_location_id' => $lastShoppingLocation,
'default_shopping_location_id' => $product->shopping_location_id,
'next_best_before_date' => $nextBestBeforeDate,
diff --git a/views/productbarcodesform.blade.php b/views/productbarcodesform.blade.php
new file mode 100644
index 00000000..c152f235
--- /dev/null
+++ b/views/productbarcodesform.blade.php
@@ -0,0 +1,68 @@
+@extends('layout.default')
+
+@if($mode == 'edit')
+ @section('title', $__t('Edit Barcodes'))
+@else
+ @section('title', $__t('Create Barcodes'))
+@endif
+
+@section('viewJsName', 'productbarcodesform')
+
+@section('content')
+
+
+
@yield('title')
+
+
+
+
+
+
+
{{ $__t('Barcode for product') }} {{ $product->name }}
+
+
+
+ @if($mode == 'edit')
+
+ @endif
+
+
+
+
+@stop
diff --git a/views/productform.blade.php b/views/productform.blade.php
index 446ad7d7..d4791bf5 100644
--- a/views/productform.blade.php
+++ b/views/productform.blade.php
@@ -346,6 +346,55 @@
+
+
+
+
+
+ |
+ {{ $__t('Barcode') }} |
+ {{ $__t('QU Factor Purchase To Stock') }} |
+ {{ $__t('Shopping Location') }} |
+
+
+
+ @if($mode == "edit")
+ @foreach($barcodes as $barcode)
+ @if($barcode->product_id == $product->id || $barcode->product_id == null)
+
+ |
+
+
+
+
+
+
+ |
+
+ {{ $barcode->barcode }}
+ |
+
+ {{ $barcode->qu_factor_purchase_to_stock }}
+ |
+ @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
+
+ @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id) !== null)
+ {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id)->name }}
+ @endif
+ |
+ @endif
+
+ @endif
+ @endforeach
+ @endif
+
+
+