From 6676e3c50882f2b742b3fb94c5767773e98f4a34 Mon Sep 17 00:00:00 2001 From: Kurt Riddlesperger Date: Fri, 24 Apr 2020 00:19:21 -0500 Subject: [PATCH] product barcodes with QU and Stores --- controllers/StockApiController.php | 12 ++ controllers/StockController.php | 30 +++++ grocy.openapi.json | 74 +++++++++++++ migrations/0103.sql | 19 ++++ public/viewjs/components/productcard.js | 9 +- public/viewjs/components/productpicker.js | 2 + public/viewjs/mealplan.js | 7 +- public/viewjs/productbarcodesform.js | 98 +++++++++++++++++ public/viewjs/productform.js | 127 ++++++++++++++++++++++ public/viewjs/purchase.js | 68 ++++++++++-- routes.php | 2 + services/StockService.php | 3 + views/productbarcodesform.blade.php | 68 ++++++++++++ views/productform.blade.php | 49 +++++++++ 14 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 migrations/0103.sql create mode 100644 public/viewjs/productbarcodesform.js create mode 100644 views/productbarcodesform.blade.php 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 + +
+ + + +
+ + +
+ + @php if($mode == 'edit') { $value = $barcode->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp + @include('components.numberpicker', array( + 'id' => 'qu_factor_purchase_to_stock', + 'label' => 'Factor purchase to stock quantity unit', + 'min' => 1, + 'value' => $value, + 'isRequired' => true, + 'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'), + 'additionalCssClasses' => 'input-group-qu', + )) + + @if(GROCY_FEATURE_FLAG_PRICE_TRACKING) +
+ + +
+ @else + + @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 Details') }} + + {{ $__t('Add') }} + +

+
+ + + + + + + + + + + @if($mode == "edit") + @foreach($barcodes as $barcode) + @if($barcode->product_id == $product->id || $barcode->product_id == null) + + + + + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + + @endif + + @endif + @endforeach + @endif + +
{{ $__t('Barcode') }}{{ $__t('QU Factor Purchase To Stock') }}{{ $__t('Shopping Location') }}
+ + + + + + + + {{ $barcode->barcode }} + + {{ $barcode->qu_factor_purchase_to_stock }} + + @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id) !== null) + {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id)->name }} + @endif +
+