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('Name') }} | +{{ $__t('Description') }} | + + @include('components.userfields_thead', array( + 'userfields' => $userfields + )) + +
---|---|---|
+ + + + + + + | ++ {{ $shoppinglocation->name }} + | ++ {{ $shoppinglocation->description }} + | + + @include('components.userfields_tbody', array( + 'userfields' => $userfields, + 'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id) + )) + +