From c64eb27ca1bf082d5af43219d62331e36cc6daa8 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Thu, 26 Jul 2018 20:27:38 +0200 Subject: [PATCH] Add something for product price tracking (references #22) --- config-dist.php | 5 ++ controllers/StockApiController.php | 20 +++++- grocy.openapi.json | 67 +++++++++++++++++ localization/de.php | 6 ++ migrations/0029.sql | 5 ++ package.json | 3 +- public/viewjs/components/productcard.js | 95 +++++++++++++++++++++++++ public/viewjs/purchase.js | 10 ++- routes.php | 1 + services/DemoDataGeneratorService.php | 83 ++++++++++++++++----- services/StockService.php | 42 ++++++++--- views/components/productcard.blade.php | 8 ++- views/layout/default.blade.php | 1 + views/purchase.blade.php | 8 ++- yarn.lock | 13 +++- 15 files changed, 335 insertions(+), 32 deletions(-) create mode 100644 migrations/0029.sql diff --git a/config-dist.php b/config-dist.php index bdfef11d..75c9cae3 100644 --- a/config-dist.php +++ b/config-dist.php @@ -7,6 +7,11 @@ Setting('MODE', 'production'); # one of the other available localization files in the "/localization" directory Setting('CULTURE', 'en'); +# To keep it simpel, grocy does not handle any currency conversions, +# this here is used to format all money values, +# so can be anything (e. g. "USD" OR "$", doesn't matter...) +Setting('CURRENCY', '$'); + # The base url of your installation, # should be just "/" when running directly under the root of a (sub)domain # or for example "https:/example.com/grocy" when using a subdirectory diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 0a9061fe..8892eeef 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -26,6 +26,18 @@ class StockApiController extends BaseApiController } } + public function ProductPriceHistory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + return $this->ApiResponse($this->StockService->GetProductPriceHistory($args['productId'])); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } + public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $bestBeforeDate = date('Y-m-d'); @@ -34,6 +46,12 @@ class StockApiController extends BaseApiController $bestBeforeDate = $request->getQueryParams()['bestbeforedate']; } + $price = null; + if (isset($request->getQueryParams()['price']) && !empty($request->getQueryParams()['price']) && is_numeric($request->getQueryParams()['price'])) + { + $price = $request->getQueryParams()['price']; + } + $transactionType = StockService::TRANSACTION_TYPE_PURCHASE; if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) { @@ -42,7 +60,7 @@ class StockApiController extends BaseApiController try { - $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType); + $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price); return $this->VoidApiActionResponse($response); } catch (\Exception $ex) diff --git a/grocy.openapi.json b/grocy.openapi.json index 0aaf59ce..403c536b 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -571,6 +571,16 @@ "type": "date" } }, + { + "in": "query", + "name": "price", + "required": false, + "description": "The price per purchase quantity unit in configured currency", + "schema": { + "type": "number", + "format": "double" + } + }, { "in": "query", "name": "transactiontype", @@ -774,6 +784,50 @@ } } }, + "/stock/get-product-price-history/{productId}": { + "get": { + "description": "Returns the price history of the given product", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of ProductPriceHistory objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductPriceHistory" + } + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + } + }, "/stock/get-current-stock": { "get": { "description": "Returns all products which are currently in stock incl. the next expiring date per product", @@ -1272,6 +1326,19 @@ } } }, + "ProductPriceHistory": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "price": { + "type": "number", + "format": "double" + } + } + }, "ExternalBarcodeLookupResponse": { "type": "object", "properties": { diff --git a/localization/de.php b/localization/de.php index 5cfecd83..db943ea8 100644 --- a/localization/de.php +++ b/localization/de.php @@ -188,6 +188,12 @@ return array( 'Habits analysis' => 'Gewohnheiten Analyse', '0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind', 'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)', + 'Last price' => 'Letzter Preis', + 'Price history' => 'Preisentwicklung', + 'No price history available' => 'Keine Preisdaten verfügbar', + 'Price' => 'Preis', + 'in #1 per purchase quantity unit' => 'in #1 pro Einkaufsmengeneinheit', + 'The price cannot be lower than #1' => 'Der Preis darf nicht niedriger als #1 sein', //Constants 'manually' => 'Manuell', diff --git a/migrations/0029.sql b/migrations/0029.sql new file mode 100644 index 00000000..793ee9bc --- /dev/null +++ b/migrations/0029.sql @@ -0,0 +1,5 @@ +ALTER TABLE stock +ADD price DECIMAL(15, 2); + +ALTER TABLE stock_log +ADD price DECIMAL(15, 2); diff --git a/package.json b/package.json index 078dd4b3..1d07b659 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "tagmanager": "https://github.com/max-favilli/tagmanager.git#3.0.2", "tempusdominus-bootstrap-4": "^5.0.1", "timeago": "^1.6.3", - "toastr": "^2.1.4" + "toastr": "^2.1.4", + "chart.js": "^2.7.2" } } diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 45ad1d7e..6b0e7c1b 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -14,6 +14,15 @@ Grocy.Components.ProductCard.Refresh = function(productId) $('#productcard-product-last-used').text((productDetails.last_used || L('never')).substring(0, 10)); $('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); + if (productDetails.last_price !== null) + { + $('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency); + } + else + { + $('#productcard-product-last-price').text(L('Unknown')); + } + EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', L('timeago_nan')); EmptyElementWhenMatches('#productcard-product-last-used-timeago', L('timeago_nan')); }, @@ -22,4 +31,90 @@ Grocy.Components.ProductCard.Refresh = function(productId) console.error(xhr); } ); + + Grocy.Api.Get('stock/get-product-price-history/' + productId, + function(priceHistoryDataPoints) + { + if (priceHistoryDataPoints.length > 0) + { + $("#productcard-product-price-history-chart").removeClass("d-none"); + $("#productcard-no-price-data-hint").addClass("d-none"); + + Grocy.Components.ProductCard.ReInitPriceHistoryChart(); + priceHistoryDataPoints.forEach((dataPoint) => + { + Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate()); + + var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0]; + dataset.data.push(dataPoint.price); + }); + Grocy.Components.ProductCard.PriceHistoryChart.update(); + } + else + { + $("#productcard-product-price-history-chart").addClass("d-none"); + $("#productcard-no-price-data-hint").removeClass("d-none"); + } + }, + function(xhr) + { + console.error(xhr); + } + ); }; + +Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() +{ + if (typeof Grocy.Components.ProductCard.PriceHistoryChart !== "undefined") + { + Grocy.Components.ProductCard.PriceHistoryChart.destroy(); + } + + var format = 'YYYY-MM-DD'; + Grocy.Components.ProductCard.PriceHistoryChart = new Chart(document.getElementById("productcard-product-price-history-chart"), { + type: "line", + data: { + labels: [ //Date objects + new Date() + // Will be populated in Grocy.Components.ProductCard.Refresh + ], + datasets: [{ + data: [ + 0 + // Will be populated in Grocy.Components.ProductCard.Refresh + ], + fill: false, + borderColor: '#17a2b8' + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + parser: format, + round: 'day', + tooltipFormat: format, + unit: 'day', + unitStepSize: 10, + displayFormats: { + 'day': format + } + }, + ticks: { + autoSkip: true, + maxRotation: 0 + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + legend: { + display: false + } + } + }); +} diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index e9fb9056..e48efa14 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -9,7 +9,13 @@ { var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock; - Grocy.Api.Get('stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + Grocy.Components.DateTimePicker.GetValue(), + var price = ""; + if (!jsonForm.price.toString().isEmpty()) + { + price = parseFloat(jsonForm.price).toFixed(2); + } + + Grocy.Api.Get('stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + Grocy.Components.DateTimePicker.GetValue() + '&price=' + price, function(result) { var addBarcode = GetUriParam('addbarcodetoselection'); @@ -43,6 +49,7 @@ else { $('#amount').val(0); + $('#price').val(''); Grocy.Components.DateTimePicker.SetValue(''); Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); @@ -74,6 +81,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) function(productDetails) { $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); + $('#price').val(productDetails.last_price); if (productDetails.product.default_best_before_days.toString() !== '0') { diff --git a/routes.php b/routes.php index c01f2d3a..d7d6f046 100644 --- a/routes.php +++ b/routes.php @@ -82,6 +82,7 @@ $app->group('/api', function() $this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct'); $this->get('/stock/inventory-product/{productId}/{newAmount}', '\Grocy\Controllers\StockApiController:InventoryProduct'); $this->get('/stock/get-product-details/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails'); + $this->get('/stock/get-product-price-history/{productId}', '\Grocy\Controllers\StockApiController:ProductPriceHistory'); $this->get('/stock/get-current-stock', '\Grocy\Controllers\StockApiController:CurrentStock'); $this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList'); $this->get('/stock/clear-shopping-list', '\Grocy\Controllers\StockApiController:ClearShoppingList'); diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index d1c9be40..95b33cdf 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -89,19 +89,71 @@ class DemoDataGeneratorService extends BaseService $this->DatabaseService->ExecuteDbStatement($sql); $stockService = new StockService(); - $stockService->AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE); - $stockService->AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE); + $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->AddMissingProductsToShoppingList(); $habitsService = new HabitsService(); @@ -126,9 +178,8 @@ class DemoDataGeneratorService extends BaseService } } - public function RecreateDemo() + private function RandomPrice() { - unlink(GROCY_DATAPATH . '/grocy.db'); - $this->PopulateDemoData(); + return mt_rand(2 * 100, 25 * 100) / 100; } } diff --git a/services/StockService.php b/services/StockService.php index 9f545eb1..5e717673 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -33,6 +33,7 @@ class StockService extends BaseService $productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date'); $quPurchase = $this->Database->quantity_units($product->qu_id_purchase); $quStock = $this->Database->quantity_units($product->qu_id_stock); + $lastPrice = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch()->price; return array( 'product' => $product, @@ -40,11 +41,31 @@ class StockService extends BaseService 'last_used' => $productLastUsed, 'stock_amount' => $productStockAmount, 'quantity_unit_purchase' => $quPurchase, - 'quantity_unit_stock' => $quStock + 'quantity_unit_stock' => $quStock, + 'last_price' => $lastPrice ); } - public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType) + public function GetProductPriceHistory(int $productId) + { + if (!$this->ProductExists($productId)) + { + throw new \Exception('Product does not exist'); + } + + $returnData = array(); + $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2', $productId, self::TRANSACTION_TYPE_PURCHASE)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); + foreach ($rows as $row) + { + $returnData[] = array( + 'date' => $row->purchased_date, + 'price' => $row->price + ); + } + return $returnData; + } + + public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price) { if (!$this->ProductExists($productId)) { @@ -59,9 +80,10 @@ class StockService extends BaseService 'product_id' => $productId, 'amount' => $amount, 'best_before_date' => $bestBeforeDate, - 'purchased_date' => date('Y-m-d'), + 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, - 'transaction_type' => $transactionType + 'transaction_type' => $transactionType, + 'price' => $price )); $logRow->save(); @@ -69,8 +91,9 @@ class StockService extends BaseService 'product_id' => $productId, 'amount' => $amount, 'best_before_date' => $bestBeforeDate, - 'purchased_date' => date('Y-m-d'), + 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, + 'price' => $price )); $stockRow->save(); @@ -116,7 +139,8 @@ class StockService extends BaseService 'used_date' => date('Y-m-d'), 'spoiled' => $spoiled, 'stock_id' => $stockEntry->stock_id, - 'transaction_type' => $transactionType + 'transaction_type' => $transactionType, + 'price' => $stockEntry->price )); $logRow->save(); @@ -133,7 +157,8 @@ class StockService extends BaseService 'used_date' => date('Y-m-d'), 'spoiled' => $spoiled, 'stock_id' => $stockEntry->stock_id, - 'transaction_type' => $transactionType + 'transaction_type' => $transactionType, + 'price' => $stockEntry->price )); $logRow->save(); @@ -165,8 +190,9 @@ class StockService extends BaseService if ($newAmount > $productStockAmount) { + $productDetails = $this->GetProductDetails($productId); $amountToAdd = $newAmount - $productStockAmount; - $this->AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); + $this->AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $productDetails['last_price']); } else if ($newAmount < $productStockAmount) { diff --git a/views/components/productcard.blade.php b/views/components/productcard.blade.php index 81df9c6f..db23546e 100644 --- a/views/components/productcard.blade.php +++ b/views/components/productcard.blade.php @@ -1,4 +1,5 @@ @push('componentScripts') + @endpush @@ -11,6 +12,11 @@ {{ $L('Stock quantity unit') }}:
{{ $L('Stock amount') }}:
{{ $L('Last purchased') }}:
- {{ $L('Last used') }}: + {{ $L('Last used') }}:
+ {{ $L('Last price') }}: + +
{{ $L('Price history') }}
+ + {{ $L('No price history available') }} diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 52ab2681..34934542 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -39,6 +39,7 @@ Grocy.LocalizationStrings = {!! json_encode($localizationStrings) !!}; Grocy.ActiveNav = '@yield('activeNav', '')'; Grocy.Culture = '{{ GROCY_CULTURE }}'; + Grocy.Currency = '{{ GROCY_CURRENCY }}'; diff --git a/views/purchase.blade.php b/views/purchase.blade.php index ac547852..b4333de8 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -30,10 +30,16 @@
- +
{{ $L('The amount cannot be lower than #1', '1') }}
+
+ + +
{{ $L('The price cannot be lower than #1', '0') }}
+
+ diff --git a/yarn.lock b/yarn.lock index a57224f5..25c4f7d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ version "5.2.0" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.2.0.tgz#50cd9856774351c56c0b1b0db4efe122d7913e58" -"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2": +"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2", "tagmanager@https://github.com/max-favilli/tagmanager.git#3.0.2": version "3.0.1" resolved "https://github.com/max-favilli/tagmanager.git#df9eb9935c8585a392dfc00602f890caf233fa94" dependencies: @@ -39,13 +39,20 @@ chart.js@2.7.1: chartjs-color "~2.2.0" moment "~2.18.0" +chart.js@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714" + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + chartjs-color-string@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1" dependencies: color-name "^1.0.0" -chartjs-color@~2.2.0: +chartjs-color@^2.1.0, chartjs-color@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae" dependencies: @@ -161,7 +168,7 @@ moment-timezone@^0.5.11: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.22.2: +"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"