From 22eaeee5729e1a6b96b59b700747016c2ee1d4ed Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 19:29:30 +0100 Subject: [PATCH 01/22] Added changelog for #660 --- changelog/57_UNRELEASED_2020-xx-xx.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index b8f0e1ed..5421d5fa 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -4,3 +4,4 @@ ### General & other improvements - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) - Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-) +- Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto) From 573b6ece895735ab1a5ba58bc367252f255bd9d7 Mon Sep 17 00:00:00 2001 From: dacto Date: Wed, 25 Mar 2020 11:30:16 -0700 Subject: [PATCH 02/22] Process `Setting`s from environment and settingoverrides values the same: (#660) * fixes handling strings representing bools from settingoverrides .txt files * trailing newline characters are now stripped --- helpers/extensions.php | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/helpers/extensions.php b/helpers/extensions.php index 5a69eef7..6cdcb203 100644 --- a/helpers/extensions.php +++ b/helpers/extensions.php @@ -138,6 +138,20 @@ function BoolToString(bool $bool) return $bool ? 'true' : 'false'; } +function ExternalSettingValue(string $value) +{ + $tvalue = rtrim($value, "\r\n"); + $lvalue = strtolower($tvalue); + if ($lvalue === "true"){ + return true; + } + elseif ($lvalue === "false") + { + return false; + } + return $tvalue; +} + function Setting(string $name, $value) { if (!defined('GROCY_' . $name)) @@ -146,22 +160,11 @@ function Setting(string $name, $value) $settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt'; if (file_exists($settingOverrideFile)) { - define('GROCY_' . $name, file_get_contents($settingOverrideFile)); + define('GROCY_' . $name, ExternalSettingValue(file_get_contents($settingOverrideFile))); } elseif (getenv('GROCY_' . $name) !== false) // An environment variable with the same name and prefix GROCY_ overwrites the given setting { - if (strtolower(getenv('GROCY_' . $name)) === "true") - { - define('GROCY_' . $name, true); - } - elseif (strtolower(getenv('GROCY_' . $name)) === "false") - { - define('GROCY_' . $name, false); - } - else - { - define('GROCY_' . $name, getenv('GROCY_' . $name)); - } + define('GROCY_' . $name, ExternalSettingValue(getenv('GROCY_'. $name))); } else { From a45317aea1f834aee05c51429e5eae2331041ac8 Mon Sep 17 00:00:00 2001 From: Immae Date: Wed, 25 Mar 2020 19:34:56 +0100 Subject: [PATCH 03/22] Add shopping location for price tracking (#658) --- controllers/StockApiController.php | 24 +++++- controllers/StockController.php | 32 ++++++++ grocy.openapi.json | 54 +++++++++++++- localization/en/strings.po | 18 +++++ localization/fr/strings.po | 18 +++++ localization/strings.pot | 18 +++++ migrations/0099.sql | 12 +++ public/viewjs/components/productcard.js | 31 +++++--- .../components/shoppinglocationpicker.js | 68 +++++++++++++++++ public/viewjs/inventory.js | 3 + public/viewjs/purchase.js | 3 + public/viewjs/shoppinglocationform.js | 69 ++++++++++++++++++ public/viewjs/shoppinglocations.js | 57 +++++++++++++++ public/viewjs/stockentryform.js | 1 + routes.php | 7 ++ services/StockService.php | 29 ++++++-- .../shoppinglocationpicker.blade.php | 20 +++++ views/inventory.blade.php | 4 + views/layout/default.blade.php | 8 ++ views/purchase.blade.php | 4 + views/shoppinglocationform.blade.php | 45 ++++++++++++ views/shoppinglocations.blade.php | 73 +++++++++++++++++++ views/stockentries.blade.php | 4 + views/stockentryform.blade.php | 4 + 24 files changed, 584 insertions(+), 22 deletions(-) create mode 100644 migrations/0099.sql create mode 100644 public/viewjs/components/shoppinglocationpicker.js create mode 100644 public/viewjs/shoppinglocationform.js create mode 100644 public/viewjs/shoppinglocations.js create mode 100644 views/components/shoppinglocationpicker.blade.php create mode 100644 views/shoppinglocationform.blade.php create mode 100644 views/shoppinglocations.blade.php diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 0a652329..5db2038b 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -82,13 +82,19 @@ class StockApiController extends BaseApiController $locationId = $requestBody['location_id']; } + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + $transactionType = StockService::TRANSACTION_TYPE_PURCHASE; if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype'])) { $transactionType = $requestBody['transactiontype']; } - $bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId); + $bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId, $shoppingLocationId); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -144,7 +150,13 @@ class StockApiController extends BaseApiController $locationId = $requestBody['location_id']; } - $bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $price, $requestBody['open'], $requestBody['purchased_date']); + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + + $bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date']); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -312,7 +324,13 @@ class StockApiController extends BaseApiController $price = $requestBody['price']; } - $bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price); + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + + $bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price, $shoppingLocationId); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) diff --git a/controllers/StockController.php b/controllers/StockController.php index 8f3698a2..bf3e4e7d 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -38,6 +38,7 @@ class StockController extends BaseController 'products' => $this->getDatabase()->products()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'), 'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(), 'nextXDays' => $nextXDays, @@ -50,6 +51,7 @@ class StockController extends BaseController { return $this->renderPage($response, 'purchase', [ 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -76,6 +78,7 @@ class StockController extends BaseController { return $this->renderPage($response, 'inventory', [ 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -85,6 +88,7 @@ class StockController extends BaseController return $this->renderPage($response, 'stockentryform', [ 'stockEntry' => $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(), 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -140,6 +144,15 @@ class StockController extends BaseController ]); } + public function ShoppingLocationsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + return $this->renderPage($response, 'shoppinglocations', [ + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations'), + 'userfieldValues' => $this->getUserfieldsService()->GetAllValues('shopping_locations') + ]); + } + public function ProductGroupsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { return $this->renderPage($response, 'productgroups', [ @@ -210,6 +223,25 @@ class StockController extends BaseController } } + public function ShoppingLocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + if ($args['shoppingLocationId'] == 'new') + { + return $this->renderPage($response, 'shoppinglocationform', [ + 'mode' => 'create', + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations') + ]); + } + else + { + return $this->renderPage($response, 'shoppinglocationform', [ + 'shoppinglocation' => $this->getDatabase()->shopping_locations($args['shoppingLocationId']), + 'mode' => 'edit', + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations') + ]); + } + } + public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { if ($args['productGroupId'] == 'new') diff --git a/grocy.openapi.json b/grocy.openapi.json index 194b4765..5c758213 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1172,6 +1172,11 @@ "format": "integer", "description": "If omitted, the default location of the product is used" }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no shopping location will be affected" + }, "purchased_date": { "type": "string", "format": "date", @@ -1478,6 +1483,11 @@ "type": "number", "format": "integer", "description": "If omitted, the default location of the product is used" + }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no shopping location will be affected" } }, "example": { @@ -1706,6 +1716,11 @@ "format": "date", "description": "The best before date which applies to added products" }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no shopping location will be affected" + }, "location_id": { "type": "number", "format": "integer", @@ -3303,6 +3318,7 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", + "shopping_locations", "recipes", "recipes_pos", "recipes_nestings", @@ -3328,6 +3344,7 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", + "shopping_locations", "recipes", "recipes_pos", "recipes_nestings", @@ -3497,6 +3514,30 @@ "row_created_timestamp": "2019-05-02 20:12:25" } }, + "ShoppingLocation": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "id": "2", + "name": "0", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25" + } + }, "StockLocation": { "type": "object", "properties": { @@ -3535,6 +3576,9 @@ "location_id": { "type": "integer" }, + "shopping_location_id": { + "type": "integer" + }, "amount": { "type": "number" }, @@ -3576,7 +3620,8 @@ "open": "0", "opened_date": null, "row_created_timestamp": "2019-05-03 18:24:04", - "location_id": "4" + "location_id": "4", + "shopping_location_id": null } }, "RecipeFulfillmentResponse": { @@ -3641,6 +3686,9 @@ "type": "number", "format": "number" }, + "last_shopping_location_id": { + "type": "integer" + }, "location": { "$ref": "#/components/schemas/Location" }, @@ -3695,6 +3743,7 @@ "plural_forms": null }, "last_price": null, + "last_shopping_location_id": null, "next_best_before_date": "2019-07-07", "location": { "id": "4", @@ -3716,6 +3765,9 @@ "price": { "type": "number", "format": "number" + }, + "shopping_location": { + "type": "string" } } }, diff --git a/localization/en/strings.po b/localization/en/strings.po index 5b5d522a..7f4a8cdf 100644 --- a/localization/en/strings.po +++ b/localization/en/strings.po @@ -66,6 +66,9 @@ msgstr "Products" msgid "Locations" msgstr "Locations" +msgid "Shopping locations" +msgstr "Shopping locations" + msgid "Quantity units" msgstr "Quantity units" @@ -162,6 +165,9 @@ msgstr "Name" msgid "Location" msgstr "Location" +msgid "Shopping location" +msgstr "Shopping location" + msgid "Min. stock amount" msgstr "Min. stock amount" @@ -201,6 +207,9 @@ msgstr "Factor purchase to stock quantity unit" msgid "Create location" msgstr "Create location" +msgid "Create shopping location" +msgstr "Create shopping location" + msgid "Create quantity unit" msgstr "Create quantity unit" @@ -234,6 +243,9 @@ msgstr "Edit product" msgid "Edit location" msgstr "Edit location" +msgid "Edit shopping location" +msgstr "Edit shopping location" + msgid "Record data" msgstr "Record data" @@ -306,6 +318,9 @@ msgstr "Are you sure to delete product \"%s\"?" msgid "Are you sure to delete location \"%s\"?" msgstr "Are you sure to delete location \"%s\"?" +msgid "Are you sure to delete shopping location \"%s\"?" +msgstr "Are you sure to delete shopping location \"%s\"?" + msgid "Manage API keys" msgstr "Manage API keys" @@ -1035,6 +1050,9 @@ msgstr "Tare weight handling enabled - please weigh the whole container, the amo msgid "You have to select a location" msgstr "You have to select a location" +msgid "You have to select a shopping location" +msgstr "You have to select a shopping location" + msgid "List" msgstr "List" diff --git a/localization/fr/strings.po b/localization/fr/strings.po index 4d0839af..7008656a 100644 --- a/localization/fr/strings.po +++ b/localization/fr/strings.po @@ -99,6 +99,9 @@ msgstr "Suivi des piles" msgid "Locations" msgstr "Emplacements" +msgid "Shopping locations" +msgstr "Commerces" + msgid "Quantity units" msgstr "Formats" @@ -198,6 +201,9 @@ msgstr "Nom" msgid "Location" msgstr "Emplacement" +msgid "Shopping location" +msgstr "Commerce" + msgid "Min. stock amount" msgstr "Quantité minimum en stock" @@ -237,6 +243,9 @@ msgstr "Facteur entre la quantité à l'achat et la quantité en stock" msgid "Create location" msgstr "Créer un emplacement" +msgid "Create shopping location" +msgstr "Créer un commerce" + msgid "Create quantity unit" msgstr "Créer un format" @@ -270,6 +279,9 @@ msgstr "Modifier le produit" msgid "Edit location" msgstr "Modifier l'emplacement" +msgid "Edit shopping location" +msgstr "Modifier le commerce" + msgid "Record data" msgstr "Enregistrer les données" @@ -347,6 +359,9 @@ msgstr "Voulez-vous vraiment supprimer le produit \"%s\" ?" msgid "Are you sure to delete location \"%s\"?" msgstr "Voulez-vous vraiment supprimer l'emplacement \"%s\" ?" +msgid "Are you sure to delete shopping location \"%s\"?" +msgstr "Voulez-vous vraiment supprimer le commerce \"%s\" ?" + msgid "Manage API keys" msgstr "Gérer les clefs API" @@ -1124,6 +1139,9 @@ msgstr "" msgid "You have to select a location" msgstr "Vous devez sélectionner un endroit" +msgid "You have to select a shopping location" +msgstr "Vous devez sélectionner un commerce" + msgid "List" msgstr "Liste" diff --git a/localization/strings.pot b/localization/strings.pot index 0cbcdba2..52cc834a 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -79,6 +79,9 @@ msgstr "" msgid "Locations" msgstr "" +msgid "Shopping locations" +msgstr "" + msgid "Quantity units" msgstr "" @@ -175,6 +178,9 @@ msgstr "" msgid "Location" msgstr "" +msgid "Shopping location" +msgstr "" + msgid "Min. stock amount" msgstr "" @@ -214,6 +220,9 @@ msgstr "" msgid "Create location" msgstr "" +msgid "Create shopping location" +msgstr "" + msgid "Create quantity unit" msgstr "" @@ -247,6 +256,9 @@ msgstr "" msgid "Edit location" msgstr "" +msgid "Edit shopping location" +msgstr "" + msgid "Record data" msgstr "" @@ -319,6 +331,9 @@ msgstr "" msgid "Are you sure to delete location \"%s\"?" msgstr "" +msgid "Are you sure to delete shopping location \"%s\"?" +msgstr "" + msgid "Manage API keys" msgstr "" @@ -1022,6 +1037,9 @@ msgstr "" msgid "You have to select a location" msgstr "" +msgid "You have to select a shopping location" +msgstr "" + msgid "List" msgstr "" diff --git a/migrations/0099.sql b/migrations/0099.sql new file mode 100644 index 00000000..764ff121 --- /dev/null +++ b/migrations/0099.sql @@ -0,0 +1,12 @@ +CREATE TABLE shopping_locations ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + description TEXT + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +ALTER TABLE stock_log +ADD shopping_location_id INTEGER; + +ALTER TABLE stock +ADD shopping_location_id INTEGER; diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 76117d83..878480fc 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -118,12 +118,25 @@ Grocy.Components.ProductCard.Refresh = function(productId) $("#productcard-no-price-data-hint").addClass("d-none"); Grocy.Components.ProductCard.ReInitPriceHistoryChart(); + var datasets = {}; + var chart = Grocy.Components.ProductCard.PriceHistoryChart.data; priceHistoryDataPoints.forEach((dataPoint) => { - Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate()); + var key = dataPoint.shopping_location || "empty"; + if (!datasets[key]) { + datasets[key] = [] + } + chart.labels.push(moment(dataPoint.date).toDate()); + datasets[key].push(dataPoint.price); - var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0]; - dataset.data.push(dataPoint.price); + }); + Object.keys(datasets).forEach((key) => { + chart.datasets.push({ + data: datasets[key], + fill: false, + borderColor: "HSL(" + (129 * chart.datasets.length) + ",100%,50%)", + label: key, + }); }); Grocy.Components.ProductCard.PriceHistoryChart.update(); } @@ -155,13 +168,9 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() labels: [ //Date objects // Will be populated in Grocy.Components.ProductCard.Refresh ], - datasets: [{ - data: [ - // Will be populated in Grocy.Components.ProductCard.Refresh - ], - fill: false, - borderColor: '%s7a2b8' - }] + datasets: [ //Datasets + // Will be populated in Grocy.Components.ProductCard.Refresh + ] }, options: { scales: { @@ -189,7 +198,7 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() }] }, legend: { - display: false + display: true } } }); diff --git a/public/viewjs/components/shoppinglocationpicker.js b/public/viewjs/components/shoppinglocationpicker.js new file mode 100644 index 00000000..cc2ae73e --- /dev/null +++ b/public/viewjs/components/shoppinglocationpicker.js @@ -0,0 +1,68 @@ +Grocy.Components.ShoppingLocationPicker = { }; + +Grocy.Components.ShoppingLocationPicker.GetPicker = function() +{ + return $('#shopping_location_id'); +} + +Grocy.Components.ShoppingLocationPicker.GetInputElement = function() +{ + return $('#shopping_location_id_text_input'); +} + +Grocy.Components.ShoppingLocationPicker.GetValue = function() +{ + return $('#shopping_location_id').val(); +} + +Grocy.Components.ShoppingLocationPicker.SetValue = function(value) +{ + Grocy.Components.ShoppingLocationPicker.GetInputElement().val(value); + Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); +} + +Grocy.Components.ShoppingLocationPicker.SetId = function(value) +{ + Grocy.Components.ShoppingLocationPicker.GetPicker().val(value); + Grocy.Components.ShoppingLocationPicker.GetPicker().data('combobox').refresh(); + Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); +} + +Grocy.Components.ShoppingLocationPicker.Clear = function() +{ + Grocy.Components.ShoppingLocationPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetId(null); +} + +$('.shopping-location-combobox').combobox({ + appendId: '_text_input', + bsVersion: '4', + clearIfNoMatch: false +}); + +var prefillByName = Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('prefill-by-name').toString(); +if (typeof prefillByName !== "undefined") +{ + possibleOptionElement = $("#shopping_location_id option:contains(\"" + prefillByName + "\")").first(); + + if (possibleOptionElement.length > 0) + { + $('#shopping_location_id').val(possibleOptionElement.val()); + $('#shopping_location_id').data('combobox').refresh(); + $('#shopping_location_id').trigger('change'); + + var nextInputElement = $(Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('next-input-selector').toString()); + nextInputElement.focus(); + } +} + +var prefillById = Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('prefill-by-id').toString(); +if (typeof prefillById !== "undefined") +{ + $('#shopping_location_id').val(prefillById); + $('#shopping_location_id').data('combobox').refresh(); + $('#shopping_location_id').trigger('change'); + + var nextInputElement = $(Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('next-input-selector').toString()); + nextInputElement.focus(); +} diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 5bed121c..8286b308 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -17,6 +17,7 @@ var jsonData = { }; jsonData.new_amount = jsonForm.new_amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); @@ -84,6 +85,7 @@ $('#price').val(''); Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('inventory-form'); @@ -150,6 +152,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) } $('#price').val(productDetails.last_price); + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { Grocy.Components.LocationPicker.SetId(productDetails.location.id); diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 91dcc3db..0ed915df 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -29,6 +29,7 @@ var jsonData = {}; jsonData.amount = amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); jsonData.price = price; if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { @@ -99,6 +100,7 @@ } Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('purchase-form'); @@ -138,6 +140,7 @@ if (Grocy.Components.ProductPicker !== undefined) function(productDetails) { $('#price').val(productDetails.last_price); + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { Grocy.Components.LocationPicker.SetId(productDetails.location.id); diff --git a/public/viewjs/shoppinglocationform.js b/public/viewjs/shoppinglocationform.js new file mode 100644 index 00000000..587981c1 --- /dev/null +++ b/public/viewjs/shoppinglocationform.js @@ -0,0 +1,69 @@ +$('#save-shopping-location-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#shoppinglocation-form').serializeJSON(); + Grocy.FrontendHelpers.BeginUiBusy("shoppinglocation-form"); + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('objects/shopping_locations', jsonData, + function(result) + { + Grocy.EditObjectId = result.created_object_id; + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/shoppinglocations'); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglocation-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + Grocy.Api.Put('objects/shopping_locations/' + Grocy.EditObjectId, jsonData, + function(result) + { + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/shoppinglocations'); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglocation-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } +}); + +$('#shoppinglocation-form input').keyup(function (event) +{ + Grocy.FrontendHelpers.ValidateForm('shoppinglocation-form'); +}); + +$('#shoppinglocation-form input').keydown(function (event) +{ + if (event.keyCode === 13) //Enter + { + event.preventDefault(); + + if (document.getElementById('shoppinglocation-form').checkValidity() === false) //There is at least one validation error + { + return false; + } + else + { + $('#save-shopping-location-button').click(); + } + } +}); + +Grocy.Components.UserfieldsForm.Load(); +$('#name').focus(); +Grocy.FrontendHelpers.ValidateForm('shoppinglocation-form'); diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js new file mode 100644 index 00000000..bea4c47b --- /dev/null +++ b/public/viewjs/shoppinglocations.js @@ -0,0 +1,57 @@ +var locationsTable = $('#shoppinglocations-table').DataTable({ + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } + ] +}); +$('#shoppinglocations-table tbody').removeClass("d-none"); +locationsTable.columns.adjust().draw(); + +$("#search").on("keyup", Delay(function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + locationsTable.search(value).draw(); +}, 200)); + +$(document).on('click', '.shoppinglocation-delete-button', function (e) +{ + var objectName = $(e.currentTarget).attr('data-shoppinglocation-name'); + var objectId = $(e.currentTarget).attr('data-shoppinglocation-id'); + + bootbox.confirm({ + message: __t('Are you sure to delete shopping location "%s"?', objectName), + 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/shopping_locations/' + objectId, {}, + function(result) + { + window.location.href = U('/shoppinglocations'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/stockentryform.js b/public/viewjs/stockentryform.js index 0492ee31..4c50e0df 100644 --- a/public/viewjs/stockentryform.js +++ b/public/viewjs/stockentryform.js @@ -14,6 +14,7 @@ jsonData.amount = jsonForm.amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); diff --git a/routes.php b/routes.php index d6a7cddf..8df89867 100644 --- a/routes.php +++ b/routes.php @@ -57,6 +57,13 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting'); } + // Stock price tracking + if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + { + $group->get('/shoppinglocations', '\Grocy\Controllers\StockController:ShoppingLocationsList'); + $group->get('/shoppinglocation/{shoppingLocationId}', '\Grocy\Controllers\StockController:ShoppingLocationEditForm'); + } + // Shopping list routes if (GROCY_FEATURE_FLAG_SHOPPINGLIST) { diff --git a/services/StockService.php b/services/StockService.php index bfde3fc2..6c2e7016 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -127,10 +127,12 @@ class StockService extends BaseService $averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = 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; + $lastShoppingLocation = $lastLogRow->shopping_location_id; } $consumeCount = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 0')->sum('amount') * -1; @@ -152,6 +154,7 @@ class StockService extends BaseService 'quantity_unit_purchase' => $quPurchase, 'quantity_unit_stock' => $quStock, 'last_price' => $lastPrice, + 'last_shopping_location_id' => $lastShoppingLocation, 'next_best_before_date' => $nextBestBeforeDate, 'location' => $location, 'average_shelf_life_days' => $averageShelfLifeDays, @@ -168,12 +171,14 @@ class StockService extends BaseService } $returnData = array(); + $shoppingLocations = $this->getDatabase()->shopping_locations(); $rows = $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)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); foreach ($rows as $row) { $returnData[] = array( 'date' => $row->purchased_date, - 'price' => $row->price + 'price' => $row->price, + 'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id)->name, ); } return $returnData; @@ -210,7 +215,7 @@ class StockService extends BaseService return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); } - public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, &$transactionId = null) + public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null) { if (!$this->ProductExists($productId)) { @@ -266,7 +271,8 @@ class StockService extends BaseService 'transaction_type' => $transactionType, 'price' => $price, 'location_id' => $locationId, - 'transaction_id' => $transactionId + 'transaction_id' => $transactionId, + 'shopping_location_id' => $shoppingLocationId, )); $logRow->save(); @@ -279,7 +285,8 @@ class StockService extends BaseService 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, 'price' => $price, - 'location_id' => $locationId + 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, )); $stockRow->save(); @@ -589,7 +596,7 @@ class StockService extends BaseService return $this->getDatabase()->lastInsertId(); } - public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $price, $open, $purchasedDate) + public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate) { $stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch(); @@ -611,6 +618,7 @@ class StockService extends BaseService 'price' => $stockRow->price, 'opened_date' => $stockRow->opened_date, 'location_id' => $stockRow->location_id, + 'shopping_location_id' => $stockRow->shopping_location_id, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id @@ -632,6 +640,7 @@ class StockService extends BaseService 'price' => $price, 'best_before_date' => $bestBeforeDate, 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, 'opened_date' => $openedDate, 'open' => $open, 'purchased_date' => $purchasedDate @@ -647,6 +656,7 @@ class StockService extends BaseService 'price' => $price, 'opened_date' => $stockRow->opened_date, 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id @@ -656,7 +666,7 @@ class StockService extends BaseService return $this->getDatabase()->lastInsertId(); } - public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null) + public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null, $shoppingLocationId = null) { if (!$this->ProductExists($productId)) { @@ -670,6 +680,11 @@ class StockService extends BaseService $price = $productDetails->last_price; } + if ($shoppingLocationId === null) + { + $shoppingLocationId = $productDetails->last_shopping_location_id; + } + // Tare weight handling // The given amount is the new total amount including the container weight (gross) // So assume that the amount in stock is the amount also including the container weight @@ -691,7 +706,7 @@ class StockService extends BaseService $bookingAmount = $newAmount; } - return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId); + return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId, $shoppingLocationId); } else if ($newAmount < $productDetails->stock_amount + $containerWeight) { diff --git a/views/components/shoppinglocationpicker.blade.php b/views/components/shoppinglocationpicker.blade.php new file mode 100644 index 00000000..8c104675 --- /dev/null +++ b/views/components/shoppinglocationpicker.blade.php @@ -0,0 +1,20 @@ +@push('componentScripts') + +@endpush + +@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp +@php if(empty($prefillById)) { $prefillById = ''; } @endphp +@php if(!isset($isRequired)) { $isRequired = false; } @endphp +@php if(empty($hint)) { $hint = ''; } @endphp +@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp + +
+ + +
{{ $__t('You have to select a shopping location') }}
+
diff --git a/views/inventory.blade.php b/views/inventory.blade.php index cef125d7..f61a30f0 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -66,6 +66,10 @@ 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'isRequired' => false )) + + @include('components.shoppinglocationpicker', array( + 'shoppinglocations' => $shoppinglocations, + )) @else @endif diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index f236d9e4..d1cecf11 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -243,6 +243,14 @@ @endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) +
  • + + + {{ $__t('Shopping locations') }} + +
  • + @endif
  • diff --git a/views/purchase.blade.php b/views/purchase.blade.php index 4611e596..7a13f347 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -30,6 +30,7 @@ 'nextInputSelector' => '#best_before_date .datetimepicker-input' )) + @php $additionalGroupCssClasses = ''; if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @@ -84,6 +85,9 @@ + @include('components.shoppinglocationpicker', array( + 'shoppinglocations' => $shoppinglocations, + )) @else @endif diff --git a/views/shoppinglocationform.blade.php b/views/shoppinglocationform.blade.php new file mode 100644 index 00000000..1b773deb --- /dev/null +++ b/views/shoppinglocationform.blade.php @@ -0,0 +1,45 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $__t('Edit shopping location')) +@else + @section('title', $__t('Create shopping location')) +@endif + +@section('viewJsName', 'shoppinglocationform') + +@section('content') +
    +
    +

    @yield('title')

    + + + + @if($mode == 'edit') + + @endif + +
    + +
    + + +
    {{ $__t('A name is required') }}
    +
    + +
    + + +
    + + @include('components.userfieldsform', array( + 'userfields' => $userfields, + 'entity' => 'shopping_locations' + )) + + + +
    +
    +
    +@stop diff --git a/views/shoppinglocations.blade.php b/views/shoppinglocations.blade.php new file mode 100644 index 00000000..12be3024 --- /dev/null +++ b/views/shoppinglocations.blade.php @@ -0,0 +1,73 @@ +@extends('layout.default') + +@section('title', $__t('Shopping locations')) +@section('activeNav', 'shoppinglocations') +@section('viewJsName', 'shoppinglocations') + +@section('content') +
    + +
    +
    + + +
    +
    + +
    +
    + + + + + + + + @include('components.userfields_thead', array( + 'userfields' => $userfields + )) + + + + + @foreach($shoppinglocations as $shoppinglocation) + + + + + + @include('components.userfields_tbody', array( + 'userfields' => $userfields, + 'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id) + )) + + + @endforeach + +
    {{ $__t('Name') }}{{ $__t('Description') }}
    + + + + + + + + {{ $shoppinglocation->name }} + + {{ $shoppinglocation->description }} +
    +
    +
    +@stop diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 7b6566fb..0f1736d5 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -35,6 +35,7 @@ {{ $__t('Amount') }} {{ $__t('Best before date') }} @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING){{ $__t('Location') }}@endif + {{ $__t('Shopping location') }} @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING){{ $__t('Price') }}@endif {{ $__t('Purchased date') }} @@ -145,6 +146,9 @@ {{ $stockEntry->price }} + + {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }} + @endif {{ $stockEntry->purchased_date }} diff --git a/views/stockentryform.blade.php b/views/stockentryform.blade.php index c763337b..dd66a4ff 100644 --- a/views/stockentryform.blade.php +++ b/views/stockentryform.blade.php @@ -65,6 +65,10 @@ 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'isRequired' => false )) + @include('components.shoppinglocationpicker', array( + 'shoppinglocations' => $shoppinglocations, + 'prefillById' => $stockEntry->shopping_location_id + )) @else @endif From c3d4be352d843fb823be70d28a834a9abbd2a431 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 19:49:10 +0100 Subject: [PATCH 04/22] Fix product card chart rendering error when there is no shopping location (references #658) --- grocy.openapi.json | 2 +- localization/strings.pot | 3 +++ public/viewjs/components/productcard.js | 7 ++++++- services/StockService.php | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/grocy.openapi.json b/grocy.openapi.json index 5c758213..70fb445f 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3767,7 +3767,7 @@ "format": "number" }, "shopping_location": { - "type": "string" + "$ref": "#/components/schemas/ShoppingLocation" } } }, diff --git a/localization/strings.pot b/localization/strings.pot index 52cc834a..f3881af5 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1762,3 +1762,6 @@ msgstr "" msgid "Group ingredients by their product group" msgstr "" + +msgid "Unknown shopping location" +msgstr "" diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 878480fc..73a1fefc 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -122,7 +122,12 @@ Grocy.Components.ProductCard.Refresh = function(productId) var chart = Grocy.Components.ProductCard.PriceHistoryChart.data; priceHistoryDataPoints.forEach((dataPoint) => { - var key = dataPoint.shopping_location || "empty"; + var key = __t("Unknown shopping location"); + if (dataPoint.shopping_location) + { + key = dataPoint.shopping_location.name + } + if (!datasets[key]) { datasets[key] = [] } diff --git a/services/StockService.php b/services/StockService.php index 6c2e7016..02c1e0a1 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -178,7 +178,7 @@ class StockService extends BaseService $returnData[] = array( 'date' => $row->purchased_date, 'price' => $row->price, - 'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id)->name, + 'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id), ); } return $returnData; From 06f65594defdeb0a249d6bb6c9e05ed3ae9829a1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 19:53:00 +0100 Subject: [PATCH 05/22] Fix stock entries page error when there is no shopping location (references #658) --- views/stockentries.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 0f1736d5..38c99e6d 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -147,7 +147,9 @@ {{ $stockEntry->price }} + @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id) !== null) {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }} + @endif @endif From 2d00f6f84a90e0e711eeaa4095cb49d8dee674a3 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:00:52 +0100 Subject: [PATCH 06/22] Immediately show the changed shopping location after edit on the stock entries page (references #658) --- public/viewjs/stockentries.js | 21 +++++++++++++++++++-- views/stockentries.blade.php | 6 +++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js index 8a1b8579..8f173a05 100644 --- a/public/viewjs/stockentries.js +++ b/public/viewjs/stockentries.js @@ -166,18 +166,35 @@ function RefreshStockEntryRow(stockRowId) function(locationResult) { locationName = locationResult.name; + + $('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id); + $('#stock-' + stockRowId + '-location').text(locationName); }, function(xhr) { console.error(xhr); } ); - $('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id); - $('#stock-' + stockRowId + '-location').text(locationName); + $('#stock-' + stockRowId + '-price').text(result.price); $('#stock-' + stockRowId + '-purchased-date').text(result.purchased_date); $('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59'); + var shoppingLocationName = ""; + Grocy.Api.Get("objects/shopping_locations/" + result.shopping_location_id, + function(shoppingLocationResult) + { + shoppingLocationName = shoppingLocationResult.name; + + $('#stock-' + stockRowId + '-shopping-location').attr('data-shopping-location-id', result.location_id); + $('#stock-' + stockRowId + '-shopping-location').text(shoppingLocationName); + }, + function (xhr) + { + console.error(xhr); + } + ); + if (result.open == 1) { $('#stock-' + stockRowId + '-opened-amount').text(__t('Opened')); diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 38c99e6d..36042ade 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -143,14 +143,14 @@ @endif @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) - - {{ $stockEntry->price }} - @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id) !== null) {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }} @endif + + {{ $stockEntry->price }} + @endif {{ $stockEntry->purchased_date }} From 5214a775edc9e0d37078f0fe9cb9c2f253f2fcf1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:02:59 +0100 Subject: [PATCH 07/22] Fix stock entries page rendering problem when FEATURE_FLAG_STOCK_PRICE_TRACKING is set to false (references #658) --- views/stockentries.blade.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 36042ade..498da5a3 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -35,8 +35,10 @@ {{ $__t('Amount') }} {{ $__t('Best before date') }} @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING){{ $__t('Location') }}@endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {{ $__t('Shopping location') }} - @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING){{ $__t('Price') }}@endif + {{ $__t('Price') }} + @endif {{ $__t('Purchased date') }} @include('components.userfields_thead', array( From 305f5b67e4f1bbe9ef71fc795c70b1dbc84d4e43 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:09:28 +0100 Subject: [PATCH 08/22] Name shopping locations "Stores" on the frontend (references #658) --- grocy.openapi.json | 6 +++--- localization/strings.pot | 18 +++++++++--------- public/viewjs/components/productcard.js | 2 +- public/viewjs/shoppinglocations.js | 2 +- .../shoppinglocationpicker.blade.php | 4 ++-- views/layout/default.blade.php | 2 +- views/shoppinglocationform.blade.php | 4 ++-- views/shoppinglocations.blade.php | 2 +- views/stockentries.blade.php | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/grocy.openapi.json b/grocy.openapi.json index 70fb445f..1f4baac7 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1175,7 +1175,7 @@ "shopping_location_id": { "type": "number", "format": "integer", - "description": "If omitted, no shopping location will be affected" + "description": "If omitted, no store will be affected" }, "purchased_date": { "type": "string", @@ -1487,7 +1487,7 @@ "shopping_location_id": { "type": "number", "format": "integer", - "description": "If omitted, no shopping location will be affected" + "description": "If omitted, no store will be affected" } }, "example": { @@ -1719,7 +1719,7 @@ "shopping_location_id": { "type": "number", "format": "integer", - "description": "If omitted, no shopping location will be affected" + "description": "If omitted, no store will be affected" }, "location_id": { "type": "number", diff --git a/localization/strings.pot b/localization/strings.pot index f3881af5..d31cd336 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -79,7 +79,7 @@ msgstr "" msgid "Locations" msgstr "" -msgid "Shopping locations" +msgid "Stores" msgstr "" msgid "Quantity units" @@ -178,9 +178,6 @@ msgstr "" msgid "Location" msgstr "" -msgid "Shopping location" -msgstr "" - msgid "Min. stock amount" msgstr "" @@ -220,7 +217,7 @@ msgstr "" msgid "Create location" msgstr "" -msgid "Create shopping location" +msgid "Create store" msgstr "" msgid "Create quantity unit" @@ -256,7 +253,7 @@ msgstr "" msgid "Edit location" msgstr "" -msgid "Edit shopping location" +msgid "Edit store" msgstr "" msgid "Record data" @@ -331,7 +328,7 @@ msgstr "" msgid "Are you sure to delete location \"%s\"?" msgstr "" -msgid "Are you sure to delete shopping location \"%s\"?" +msgid "Are you sure to delete store \"%s\"?" msgstr "" msgid "Manage API keys" @@ -1037,7 +1034,7 @@ msgstr "" msgid "You have to select a location" msgstr "" -msgid "You have to select a shopping location" +msgid "You have to select a store" msgstr "" msgid "List" @@ -1763,5 +1760,8 @@ msgstr "" msgid "Group ingredients by their product group" msgstr "" -msgid "Unknown shopping location" +msgid "Unknown store" +msgstr "" + +msgid "Store" msgstr "" diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 73a1fefc..951207d1 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -122,7 +122,7 @@ Grocy.Components.ProductCard.Refresh = function(productId) var chart = Grocy.Components.ProductCard.PriceHistoryChart.data; priceHistoryDataPoints.forEach((dataPoint) => { - var key = __t("Unknown shopping location"); + var key = __t("Unknown store"); if (dataPoint.shopping_location) { key = dataPoint.shopping_location.name diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js index bea4c47b..972bce7d 100644 --- a/public/viewjs/shoppinglocations.js +++ b/public/viewjs/shoppinglocations.js @@ -25,7 +25,7 @@ $(document).on('click', '.shoppinglocation-delete-button', function (e) var objectId = $(e.currentTarget).attr('data-shoppinglocation-id'); bootbox.confirm({ - message: __t('Are you sure to delete shopping location "%s"?', objectName), + message: __t('Are you sure to delete store "%s"?', objectName), closeButton: false, buttons: { confirm: { diff --git a/views/components/shoppinglocationpicker.blade.php b/views/components/shoppinglocationpicker.blade.php index 8c104675..f3ccf237 100644 --- a/views/components/shoppinglocationpicker.blade.php +++ b/views/components/shoppinglocationpicker.blade.php @@ -9,12 +9,12 @@ @php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp
    - + -
    {{ $__t('You have to select a shopping location') }}
    +
    {{ $__t('You have to select a store') }}
    diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index d1cecf11..0ac57062 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -247,7 +247,7 @@
  • - {{ $__t('Shopping locations') }} + {{ $__t('Stores') }}
  • @endif diff --git a/views/shoppinglocationform.blade.php b/views/shoppinglocationform.blade.php index 1b773deb..bbe1a35f 100644 --- a/views/shoppinglocationform.blade.php +++ b/views/shoppinglocationform.blade.php @@ -1,9 +1,9 @@ @extends('layout.default') @if($mode == 'edit') - @section('title', $__t('Edit shopping location')) + @section('title', $__t('Edit store')) @else - @section('title', $__t('Create shopping location')) + @section('title', $__t('Create store')) @endif @section('viewJsName', 'shoppinglocationform') diff --git a/views/shoppinglocations.blade.php b/views/shoppinglocations.blade.php index 12be3024..ed49f777 100644 --- a/views/shoppinglocations.blade.php +++ b/views/shoppinglocations.blade.php @@ -1,6 +1,6 @@ @extends('layout.default') -@section('title', $__t('Shopping locations')) +@section('title', $__t('Stores')) @section('activeNav', 'shoppinglocations') @section('viewJsName', 'shoppinglocations') diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 498da5a3..36fda7bb 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -36,7 +36,7 @@ {{ $__t('Best before date') }} @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING){{ $__t('Location') }}@endif @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) - {{ $__t('Shopping location') }} + {{ $__t('Store') }} {{ $__t('Price') }} @endif {{ $__t('Purchased date') }} From 2619d03cc0bd0ce4d76aafd4262470dcdb7abf9f Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:12:38 +0100 Subject: [PATCH 09/22] Added changelog for #658 --- changelog/57_UNRELEASED_2020-xx-xx.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index 5421d5fa..3e6bf6de 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -1,3 +1,9 @@ +### New feature: Price history per store +- Define stores under master data +- Track on purchase/inventory in which store you bought the product +- => The price history chart on the product card shows a line per store +- (Thanks @immae) + ### Recipe fixes - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) From f3b504b7de0c4d337f7b4019e2f0bb4580556b24 Mon Sep 17 00:00:00 2001 From: Mik- Date: Wed, 25 Mar 2020 20:26:54 +0100 Subject: [PATCH 10/22] Optionally use number pad input in best-before-date fields on mobile Chrome (#657) * Add missing translation for de * Add option to set best-before-date field input-type to number This allows faster input in mobile devices * Fix datetimepicker with inputmode * Fix datatimepicker with numeric input * Add missing translation for de * Add option to set best-before-date field input-type to number This allows faster input in mobile devices * Fix datetimepicker with inputmode * Fix datatimepicker with numeric input * Use more precise name for number pad option * Update localization/de/strings.po Co-Authored-By: Bernd Bestel * Fix merge conflict * Fix merge conflict try 2 Co-authored-by: Michael Neuendorf Co-authored-by: Bernd Bestel --- config-dist.php | 1 + localization/strings.pot | 3 +++ views/components/datetimepicker.blade.php | 3 ++- views/inventory.blade.php | 3 ++- views/purchase.blade.php | 3 ++- views/stockentryform.blade.php | 3 ++- 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/config-dist.php b/config-dist.php index df69c012..a9a1f758 100644 --- a/config-dist.php +++ b/config-dist.php @@ -145,6 +145,7 @@ Setting('FEATURE_FLAG_CALENDAR', true); Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true); Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true); +Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in best-before-date fields on (supported) mobile browsers Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true); Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true); diff --git a/localization/strings.pot b/localization/strings.pot index d31cd336..4a0da6ba 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1765,3 +1765,6 @@ msgstr "" msgid "Store" msgstr "" + +msgid "Transaction successfully undone" +msgstr "" diff --git a/views/components/datetimepicker.blade.php b/views/components/datetimepicker.blade.php index de2d227e..d3ca83bc 100644 --- a/views/components/datetimepicker.blade.php +++ b/views/components/datetimepicker.blade.php @@ -14,6 +14,7 @@ @php if(!isset($nextInputSelector)) { $nextInputSelector = false; } @endphp @php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp @php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp +@php if(empty($activateNumberPad)) { $activateNumberPad = false; } @endphp
    - 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp diff --git a/views/purchase.blade.php b/views/purchase.blade.php index 7a13f347..f8e38489 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -52,7 +52,8 @@ 'shortcutLabel' => 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp diff --git a/views/stockentryform.blade.php b/views/stockentryform.blade.php index dd66a4ff..97f372e3 100644 --- a/views/stockentryform.blade.php +++ b/views/stockentryform.blade.php @@ -40,7 +40,8 @@ 'shortcutLabel' => 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp From 7b737590ea9d6b8f4005eb883e9a4bd98c77249c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:32:37 +0100 Subject: [PATCH 11/22] Added changelog for #657 --- changelog/57_UNRELEASED_2020-xx-xx.md | 1 + config-dist.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index 3e6bf6de..e9eb49be 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -8,6 +8,7 @@ - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) ### General & other improvements +- New `config.php` setting `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD` which activates the number pad for best-before-date fields on (supported) mobile browsers (useful because of [shorthands](https://github.com/grocy/grocy#input-shorthands-for-date-fields)) (defaults to `true`) (thanks @Mik-) - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) - Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-) - Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto) diff --git a/config-dist.php b/config-dist.php index a9a1f758..f6da7527 100644 --- a/config-dist.php +++ b/config-dist.php @@ -145,9 +145,9 @@ Setting('FEATURE_FLAG_CALENDAR', true); Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true); Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true); -Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in best-before-date fields on (supported) mobile browsers Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true); +Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in best-before-date fields on (supported) mobile browsers Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true); Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true); From 4f40b40fe0e482f8c00be249effaa16b36269908 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:39:19 +0100 Subject: [PATCH 12/22] Fixed migration (references #658) --- migrations/0099.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0099.sql b/migrations/0099.sql index 764ff121..b79ed372 100644 --- a/migrations/0099.sql +++ b/migrations/0099.sql @@ -1,7 +1,7 @@ CREATE TABLE shopping_locations ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, name TEXT NOT NULL UNIQUE, - description TEXT + description TEXT, row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) ); From d7738aa1ec330c81f11e4976681df0299d4ed35a Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 25 Mar 2020 20:56:33 +0100 Subject: [PATCH 13/22] Added some demo data to showcase the new price tracking per store feature (references #658) --- localization/demo_data.pot | 8 ++ localization/en/demo_data.po | 6 + services/DemoDataGeneratorService.php | 168 ++++++++++++++------------ 3 files changed, 108 insertions(+), 74 deletions(-) diff --git a/localization/demo_data.pot b/localization/demo_data.pot index 282e5160..bd7e4d02 100644 --- a/localization/demo_data.pot +++ b/localization/demo_data.pot @@ -345,3 +345,11 @@ msgstr "" msgid "Portuguese (Portugal)" msgstr "" + +# Use a in your country well known supermarket name +msgid "DemoSupermarket1" +msgstr "" + +# Use a in your country well known supermarket name +msgid "DemoSupermarket2" +msgstr "" diff --git a/localization/en/demo_data.po b/localization/en/demo_data.po index 0d629661..ea4c0cc0 100644 --- a/localization/en/demo_data.po +++ b/localization/en/demo_data.po @@ -284,3 +284,9 @@ msgstr "Swedish" msgid "Polish" msgstr "Polish" + +msgid "DemoSupermarket1" +msgstr "Walmart" + +msgid "DemoSupermarket2" +msgstr "Kroger" diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 6a32b9e3..29d426a7 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -41,6 +41,9 @@ class DemoDataGeneratorService extends BaseService INSERT INTO locations (name) VALUES ('{$this->__t_sql('Tinned food cupboard')}'); --5 INSERT INTO locations (name, is_freezer) VALUES ('{$this->__t_sql('Freezer')}', 1); --6 + INSERT INTO shopping_locations (name) VALUES ('{$this->__t_sql('DemoSupermarket1')}'); --1 + INSERT INTO shopping_locations (name) VALUES ('{$this->__t_sql('DemoSupermarket2')}'); --2 + DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Glass')}'; INSERT INTO quantity_units (id, name, name_plural) VALUES (4, '{$this->__n_sql(1, 'Glass', 'Glasses')}', '{$this->__n_sql(2, 'Glass', 'Glasses')}'); --4 DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Tin')}'; @@ -186,80 +189,80 @@ class DemoDataGeneratorService extends BaseService $this->getDatabaseService()->ExecuteDbStatement($sql); $stockService = new StockService(); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); $stockService->AddMissingProductsToShoppingList(); $stockService->OpenProduct(3, 1); $stockService->OpenProduct(6, 1); @@ -317,6 +320,23 @@ class DemoDataGeneratorService extends BaseService return mt_rand(2 * 100, 25 * 100) / 100; } + private $LastSupermarketId = 1; + private function NextSupermarketId() + { + $returnValue = $this->LastSupermarketId; + + if ($this->LastSupermarketId == 1) + { + $this->LastSupermarketId = 2; + } + else + { + $this->LastSupermarketId = 1; + } + + return $returnValue; + } + private function __t_sql(string $text) { $localizedText = $this->getLocalizationService()->__t($text, null); From 9a27b8e3a55415b8dfec648c6f51d9f6eb937fde Mon Sep 17 00:00:00 2001 From: Marc Ole Bulling Date: Fri, 27 Mar 2020 14:29:26 +0100 Subject: [PATCH 14/22] Added amount value for locations api (#668) --- grocy.openapi.json | 4 ++++ migrations/0099.sql | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/grocy.openapi.json b/grocy.openapi.json index 1f4baac7..be1bdb71 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3547,6 +3547,9 @@ "product_id": { "type": "integer" }, + "amount": { + "type": "integer" + }, "location_id": { "type": "integer" }, @@ -3560,6 +3563,7 @@ "example": { "id": "1", "product_id": "3", + "amount": "2", "location_id": "1", "name": "Fridge" } diff --git a/migrations/0099.sql b/migrations/0099.sql index b79ed372..d35f069e 100644 --- a/migrations/0099.sql +++ b/migrations/0099.sql @@ -10,3 +10,18 @@ ADD shopping_location_id INTEGER; ALTER TABLE stock ADD shopping_location_id INTEGER; + +DROP VIEW stock_current_locations; +CREATE VIEW stock_current_locations +AS +SELECT + 1 AS id, -- Dummy, LessQL needs an id column + s.product_id, + SUM(s.amount) as amount, + s.location_id AS location_id, + l.name AS location_name, + l.is_freezer AS location_is_freezer +FROM stock s +JOIN locations l + ON s.location_id = l.id +GROUP BY s.product_id, s.location_id, l.name; From 646f638111e8afe0bed8cb150e31f4596b77e5ff Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 27 Mar 2020 14:32:30 +0100 Subject: [PATCH 15/22] Added changelog for #668 --- changelog/57_UNRELEASED_2020-xx-xx.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index e9eb49be..3f5b5015 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -7,6 +7,9 @@ ### Recipe fixes - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) +### API improvements +- The endpoint `/stock/products/{productId}/locations` now also returns the current stock amount of the product in that loctation (new field/property `amount`) (thanks @Forceu) + ### General & other improvements - New `config.php` setting `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD` which activates the number pad for best-before-date fields on (supported) mobile browsers (useful because of [shorthands](https://github.com/grocy/grocy#input-shorthands-for-date-fields)) (defaults to `true`) (thanks @Mik-) - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) From 81d84d93e2459ed099f961413005114a6dcf2c3e Mon Sep 17 00:00:00 2001 From: tsia Date: Fri, 27 Mar 2020 17:44:29 +0100 Subject: [PATCH 16/22] fixed missing class error in CalendarApiController (#669) --- controllers/CalendarApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/CalendarApiController.php b/controllers/CalendarApiController.php index 183d6245..e20c3cb3 100644 --- a/controllers/CalendarApiController.php +++ b/controllers/CalendarApiController.php @@ -52,7 +52,7 @@ class CalendarApiController extends BaseApiController try { return $this->ApiResponse($response, array( - 'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL)) + 'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(\Grocy\Services\ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL)) )); } catch (\Exception $ex) From d509f9add0f15f7a59d253901ecef72c5bae041c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 27 Mar 2020 17:46:08 +0100 Subject: [PATCH 17/22] Added changelog for #669 --- changelog/57_UNRELEASED_2020-xx-xx.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index 3f5b5015..bca1a4cb 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -7,6 +7,9 @@ ### Recipe fixes - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) +### Calendar fixes +- Fixed that the "Share/Integrate calendar (iCal)" button did not work (thanks @tsia) + ### API improvements - The endpoint `/stock/products/{productId}/locations` now also returns the current stock amount of the product in that loctation (new field/property `amount`) (thanks @Forceu) From 2fee4b45ff29acb10701dde742aaae2de74c3f1c Mon Sep 17 00:00:00 2001 From: kriddles <54413450+kriddles@users.noreply.github.com> Date: Fri, 27 Mar 2020 13:27:40 -0500 Subject: [PATCH 18/22] set default store for product, purchase set last store purchased if available or use product default (#672) --- controllers/StockController.php | 2 ++ grocy.openapi.json | 9 +++++++-- migrations/0099.sql | 3 +++ public/viewjs/purchase.js | 11 ++++++++++- services/StockService.php | 2 ++ views/components/shoppinglocationpicker.blade.php | 2 +- views/inventory.blade.php | 3 ++- views/productform.blade.php | 9 +++++++++ views/purchase.blade.php | 3 ++- views/stockentryform.blade.php | 1 + 10 files changed, 39 insertions(+), 6 deletions(-) diff --git a/controllers/StockController.php b/controllers/StockController.php index bf3e4e7d..f42546e6 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'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), 'products' => $this->getDatabase()->products()->where('parent_product_id IS NULL')->orderBy('name'), @@ -194,6 +195,7 @@ class StockController extends BaseController 'product' => $product, 'locations' => $this->getDatabase()->locations()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), 'products' => $this->getDatabase()->products()->where('id != :1 AND parent_product_id IS NULL', $product->id)->orderBy('name'), diff --git a/grocy.openapi.json b/grocy.openapi.json index be1bdb71..3797efd3 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3435,6 +3435,9 @@ "row_created_timestamp": { "type": "string", "format": "date-time" + }, + "shopping_location_id": { + "type": "integer" } }, "example": { @@ -3455,7 +3458,8 @@ "allow_partial_units_in_stock": "0", "enable_tare_weight_handling": "0", "tare_weight": "0.0", - "not_check_stock_fulfillment_for_recipes": "0" + "not_check_stock_fulfillment_for_recipes": "0", + "shopping_location_id": null } }, "QuantityUnit": { @@ -3724,7 +3728,8 @@ "allow_partial_units_in_stock": "0", "enable_tare_weight_handling": "0", "tare_weight": "0.0", - "not_check_stock_fulfillment_for_recipes": "0" + "not_check_stock_fulfillment_for_recipes": "0", + "last_shopping_location_id": null }, "last_purchased": null, "last_used": null, diff --git a/migrations/0099.sql b/migrations/0099.sql index d35f069e..b7d90f92 100644 --- a/migrations/0099.sql +++ b/migrations/0099.sql @@ -11,6 +11,9 @@ ADD shopping_location_id INTEGER; ALTER TABLE stock ADD shopping_location_id INTEGER; +ALTER TABLE products +ADD shopping_location_id INTEGER; + DROP VIEW stock_current_locations; CREATE VIEW stock_current_locations AS diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 0ed915df..e4e8e3b3 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -140,7 +140,16 @@ if (Grocy.Components.ProductPicker !== undefined) function(productDetails) { $('#price').val(productDetails.last_price); - Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); + + if (productDetails.last_shopping_location_id != null) + { + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); + } + else + { + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.default_shopping_location_id); + } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { Grocy.Components.LocationPicker.SetId(productDetails.location.id); diff --git a/services/StockService.php b/services/StockService.php index 02c1e0a1..2b794bf2 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -127,6 +127,7 @@ class StockService extends BaseService $averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = 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)) @@ -155,6 +156,7 @@ class StockService extends BaseService 'quantity_unit_stock' => $quStock, 'last_price' => $lastPrice, 'last_shopping_location_id' => $lastShoppingLocation, + 'default_shopping_location_id' => $product->shopping_location_id, 'next_best_before_date' => $nextBestBeforeDate, 'location' => $location, 'average_shelf_life_days' => $averageShelfLifeDays, diff --git a/views/components/shoppinglocationpicker.blade.php b/views/components/shoppinglocationpicker.blade.php index f3ccf237..83aaf767 100644 --- a/views/components/shoppinglocationpicker.blade.php +++ b/views/components/shoppinglocationpicker.blade.php @@ -9,7 +9,7 @@ @php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp
    - + diff --git a/views/productform.blade.php b/views/productform.blade.php index 8b74dd29..097ec69b 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -88,6 +88,15 @@ @endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + @include('components.shoppinglocationpicker', array( + 'label' => 'Default store', + 'shoppinglocations' => $shoppinglocations + )) + @else + + @endif + @php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp @include('components.numberpicker', array( 'id' => 'min_stock_amount', diff --git a/views/purchase.blade.php b/views/purchase.blade.php index f8e38489..aa753772 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -87,7 +87,8 @@
    @include('components.shoppinglocationpicker', array( - 'shoppinglocations' => $shoppinglocations, + 'label' => 'Store', + 'shoppinglocations' => $shoppinglocations )) @else diff --git a/views/stockentryform.blade.php b/views/stockentryform.blade.php index 97f372e3..9b6035f3 100644 --- a/views/stockentryform.blade.php +++ b/views/stockentryform.blade.php @@ -67,6 +67,7 @@ 'isRequired' => false )) @include('components.shoppinglocationpicker', array( + 'label' => 'Store', 'shoppinglocations' => $shoppinglocations, 'prefillById' => $stockEntry->shopping_location_id )) From caf7127c13a90fcb032aae6b7f8ae5e1fbc7fc03 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 27 Mar 2020 19:32:25 +0100 Subject: [PATCH 19/22] Added changelog for #672 --- changelog/57_UNRELEASED_2020-xx-xx.md | 5 +++-- localization/strings.pot | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index bca1a4cb..bd6c811f 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -1,8 +1,9 @@ ### New feature: Price history per store - Define stores under master data -- Track on purchase/inventory in which store you bought the product +- New product option to set the default store +- Track on purchase/inventory in which store you bought the product (gets prefilled by the last store you purchased the product, or the default store of the product if you never bought it) - => The price history chart on the product card shows a line per store -- (Thanks @immae) +- (Thanks @immae and @kriddles) ### Recipe fixes - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) diff --git a/localization/strings.pot b/localization/strings.pot index 4a0da6ba..bcb38911 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1768,3 +1768,6 @@ msgstr "" msgid "Transaction successfully undone" msgstr "" + +msgid "Default store" +msgstr "" From f66a4c96317ed18c14fdd6e88a1bf3a20d086c5e Mon Sep 17 00:00:00 2001 From: Mik- Date: Sun, 29 Mar 2020 14:25:04 +0200 Subject: [PATCH 20/22] Camera barcode scanner enhancements (#675) * Disable torch button, if not supported * Allow toggling torch * Don't exceed the screen width with camera window * Disable torch button, if not supported * Allow toggling torch * Don't exceed the screen width with camera window * Allow toggling torch * Disable torch button, if not supported * Allow toggling torch * Don't exceed the screen width with camera window * Allow toggling torch * Allow toggling torch * Don't exceed the screen width with camera window * Remove toggling of torch, as it's not working and add resize of video to fit in viewport hieght * Add feature to always turn on the torch in camera scanner. * Fix feature flag name Co-authored-by: Michael Neuendorf --- config-dist.php | 1 + public/css/grocy.css | 15 ++++++ public/viewjs/components/barcodescanner.js | 59 ++++++++++++++++++++-- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/config-dist.php b/config-dist.php index f6da7527..82ae183e 100644 --- a/config-dist.php +++ b/config-dist.php @@ -154,3 +154,4 @@ Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true); # Feature settings Setting('FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT', true); // When set to false, opened products will not be considered for minimum stock amounts +Setting('FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA', true); // Enables the torch automaticaly in every camera barcode scanner. diff --git a/public/css/grocy.css b/public/css/grocy.css index 68bc85ae..957bdf13 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -259,6 +259,21 @@ input::-webkit-inner-spin-button { color: inherit; } +/* Barcodescanner Quagga */ +#barcodescanner-container { + max-height: 90vw; +} +#livestream-container { + max-height: 100%; +} +#barcodescanner-livestream video { + width: 100%; + +} +#barcodescanner-livestream canvas { + width: 100%; +} + /* Third party component customizations - Bootstrap */ /* Hide the form validation feedback icons introduced in Bootstrap 4.2.0 - a colored border is enough */ diff --git a/public/viewjs/components/barcodescanner.js b/public/viewjs/components/barcodescanner.js index 5db834fa..dc21c717 100644 --- a/public/viewjs/components/barcodescanner.js +++ b/public/viewjs/components/barcodescanner.js @@ -1,5 +1,42 @@ Grocy.Components.BarcodeScanner = { }; +Grocy.Components.BarcodeScanner.CheckCapabilities = function() +{ + var track = Quagga.CameraAccess.getActiveTrack(); + var capabilities = {}; + if (typeof track.getCapabilities === 'function') { + capabilities = track.getCapabilities(); + } + + // Check if the camera is capable to turn on a torch. + var canTorch = typeof capabilities.torch === 'boolean' && capabilities.torch + // Remove the torch button, if either the device can not torch or AutoTorchOn is set. + var node = document.querySelector('.torch'); + if (node) { + node.style.display = canTorch && !Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none'; + } + // If AutoTorchOn is set, turn on the torch. + if (canTorch && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) { + Grocy.Components.BarcodeScanner.TorchOn(track); + } + + // Reduce the height of the video, if it's heigher than then the viewport + var bc = document.getElementById('barcodescanner-container'); + if (bc) { + var bcAspectRatio = bc.offsetWidth / bc.offsetHeight; + var settings = track.getSettings(); + if (bcAspectRatio > settings.aspectRatio) { + var v = document.querySelector('#barcodescanner-livestream video') + if (v) { + var c = document.querySelector('#barcodescanner-livestream canvas') + var newWidth = v.clientWidth / bcAspectRatio * settings.aspectRatio + 'px'; + v.style.width = newWidth; + c.style.width = newWidth; + } + } + } +} + Grocy.Components.BarcodeScanner.StartScanning = function() { Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; @@ -11,8 +48,6 @@ Grocy.Components.BarcodeScanner.StartScanning = function() type: "LiveStream", target: document.querySelector("#barcodescanner-livestream"), constraints: { - width: 436, - height: 327, facingMode: "environment" } }, @@ -70,6 +105,9 @@ Grocy.Components.BarcodeScanner.StartScanning = function() }, 500); return; } + + Grocy.Components.BarcodeScanner.CheckCapabilities(); + Quagga.start(); }); } @@ -84,6 +122,19 @@ Grocy.Components.BarcodeScanner.StopScanning = function() bootbox.hideAll(); } +Grocy.Components.BarcodeScanner.TorchOn = function(track) +{ + if (track) { + track.applyConstraints({ + advanced: [ + { + torch: true + } + ] + }); + } +} + Quagga.onDetected(function(result) { $.each(result.codeResult.decodedCodes, function(id, error) @@ -157,10 +208,10 @@ $(document).on("click", "#barcodescanner-start-button", function(e) buttons: { torch: { label: '', - className: 'btn-warning responsive-button', + className: 'btn-warning responsive-button torch', callback: function() { - Quagga.CameraAccess.getActiveTrack().applyConstraints({ advanced: [{ torch: true }] }); + Grocy.Components.BarcodeScanner.TorchOn(Quagga.CameraAccess.getActiveTrack()); return false; } }, From f092c8b10d7f9d4a7e275b38534d78c8ecce0817 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 29 Mar 2020 14:29:38 +0200 Subject: [PATCH 21/22] Added changelog for #675 --- changelog/57_UNRELEASED_2020-xx-xx.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index bd6c811f..60dc9a85 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -16,6 +16,10 @@ ### General & other improvements - New `config.php` setting `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD` which activates the number pad for best-before-date fields on (supported) mobile browsers (useful because of [shorthands](https://github.com/grocy/grocy#input-shorthands-for-date-fields)) (defaults to `true`) (thanks @Mik-) +- Enhancements for the camera barcode scanner (thanks @Mik-) + - The light button only displayed when the device has a flash light + - New `config.php` setting `FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA` to always enable the flash light automatically + - Various display/CSS improvements - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) - Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-) - Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto) From 8e8252573215b76b1632100de86a83e66f7afeeb Mon Sep 17 00:00:00 2001 From: tsia Date: Sun, 29 Mar 2020 18:15:27 +0200 Subject: [PATCH 22/22] fix api endpoint url in comment (#676) --- data/plugins/DemoBarcodeLookupPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/plugins/DemoBarcodeLookupPlugin.php b/data/plugins/DemoBarcodeLookupPlugin.php index ae2095f0..3d94c832 100644 --- a/data/plugins/DemoBarcodeLookupPlugin.php +++ b/data/plugins/DemoBarcodeLookupPlugin.php @@ -14,7 +14,7 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin /* To try it: - Call the API function at /api/stock/external-barcode-lookup/{barcode} + Call the API function at /api/stock/barcodes/external-lookup/{barcode} When you also add ?add=true as a query parameter to the API call, on a successful lookup the product is added to the database and in the output