Add shopping location for price tracking (#658)

This commit is contained in:
Immae
2020-03-25 19:34:56 +01:00
committed by GitHub
parent 573b6ece89
commit a45317aea1
24 changed files with 584 additions and 22 deletions

View File

@@ -82,13 +82,19 @@ class StockApiController extends BaseApiController
$locationId = $requestBody['location_id']; $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; $transactionType = StockService::TRANSACTION_TYPE_PURCHASE;
if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype'])) if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype']))
{ {
$transactionType = $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)); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)
@@ -144,7 +150,13 @@ class StockApiController extends BaseApiController
$locationId = $requestBody['location_id']; $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)); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)
@@ -312,7 +324,13 @@ class StockApiController extends BaseApiController
$price = $requestBody['price']; $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)); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)

View File

@@ -38,6 +38,7 @@ class StockController extends BaseController
'products' => $this->getDatabase()->products()->orderBy('name'), 'products' => $this->getDatabase()->products()->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'), 'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'),
'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(), 'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(),
'nextXDays' => $nextXDays, 'nextXDays' => $nextXDays,
@@ -50,6 +51,7 @@ class StockController extends BaseController
{ {
return $this->renderPage($response, 'purchase', [ return $this->renderPage($response, 'purchase', [
'products' => $this->getDatabase()->products()->orderBy('name'), 'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name') 'locations' => $this->getDatabase()->locations()->orderBy('name')
]); ]);
} }
@@ -76,6 +78,7 @@ class StockController extends BaseController
{ {
return $this->renderPage($response, 'inventory', [ return $this->renderPage($response, 'inventory', [
'products' => $this->getDatabase()->products()->orderBy('name'), 'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name') 'locations' => $this->getDatabase()->locations()->orderBy('name')
]); ]);
} }
@@ -85,6 +88,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'stockentryform', [ return $this->renderPage($response, 'stockentryform', [
'stockEntry' => $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(), 'stockEntry' => $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(),
'products' => $this->getDatabase()->products()->orderBy('name'), 'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->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) public function ProductGroupsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'productgroups', [ 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) public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['productGroupId'] == 'new') if ($args['productGroupId'] == 'new')

View File

@@ -1172,6 +1172,11 @@
"format": "integer", "format": "integer",
"description": "If omitted, the default location of the product is used" "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": { "purchased_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
@@ -1478,6 +1483,11 @@
"type": "number", "type": "number",
"format": "integer", "format": "integer",
"description": "If omitted, the default location of the product is used" "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": { "example": {
@@ -1706,6 +1716,11 @@
"format": "date", "format": "date",
"description": "The best before date which applies to added products" "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": { "location_id": {
"type": "number", "type": "number",
"format": "integer", "format": "integer",
@@ -3303,6 +3318,7 @@
"quantity_unit_conversions", "quantity_unit_conversions",
"shopping_list", "shopping_list",
"shopping_lists", "shopping_lists",
"shopping_locations",
"recipes", "recipes",
"recipes_pos", "recipes_pos",
"recipes_nestings", "recipes_nestings",
@@ -3328,6 +3344,7 @@
"quantity_unit_conversions", "quantity_unit_conversions",
"shopping_list", "shopping_list",
"shopping_lists", "shopping_lists",
"shopping_locations",
"recipes", "recipes",
"recipes_pos", "recipes_pos",
"recipes_nestings", "recipes_nestings",
@@ -3497,6 +3514,30 @@
"row_created_timestamp": "2019-05-02 20:12:25" "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": { "StockLocation": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -3535,6 +3576,9 @@
"location_id": { "location_id": {
"type": "integer" "type": "integer"
}, },
"shopping_location_id": {
"type": "integer"
},
"amount": { "amount": {
"type": "number" "type": "number"
}, },
@@ -3576,7 +3620,8 @@
"open": "0", "open": "0",
"opened_date": null, "opened_date": null,
"row_created_timestamp": "2019-05-03 18:24:04", "row_created_timestamp": "2019-05-03 18:24:04",
"location_id": "4" "location_id": "4",
"shopping_location_id": null
} }
}, },
"RecipeFulfillmentResponse": { "RecipeFulfillmentResponse": {
@@ -3641,6 +3686,9 @@
"type": "number", "type": "number",
"format": "number" "format": "number"
}, },
"last_shopping_location_id": {
"type": "integer"
},
"location": { "location": {
"$ref": "#/components/schemas/Location" "$ref": "#/components/schemas/Location"
}, },
@@ -3695,6 +3743,7 @@
"plural_forms": null "plural_forms": null
}, },
"last_price": null, "last_price": null,
"last_shopping_location_id": null,
"next_best_before_date": "2019-07-07", "next_best_before_date": "2019-07-07",
"location": { "location": {
"id": "4", "id": "4",
@@ -3716,6 +3765,9 @@
"price": { "price": {
"type": "number", "type": "number",
"format": "number" "format": "number"
},
"shopping_location": {
"type": "string"
} }
} }
}, },

View File

@@ -66,6 +66,9 @@ msgstr "Products"
msgid "Locations" msgid "Locations"
msgstr "Locations" msgstr "Locations"
msgid "Shopping locations"
msgstr "Shopping locations"
msgid "Quantity units" msgid "Quantity units"
msgstr "Quantity units" msgstr "Quantity units"
@@ -162,6 +165,9 @@ msgstr "Name"
msgid "Location" msgid "Location"
msgstr "Location" msgstr "Location"
msgid "Shopping location"
msgstr "Shopping location"
msgid "Min. stock amount" msgid "Min. stock amount"
msgstr "Min. stock amount" msgstr "Min. stock amount"
@@ -201,6 +207,9 @@ msgstr "Factor purchase to stock quantity unit"
msgid "Create location" msgid "Create location"
msgstr "Create location" msgstr "Create location"
msgid "Create shopping location"
msgstr "Create shopping location"
msgid "Create quantity unit" msgid "Create quantity unit"
msgstr "Create quantity unit" msgstr "Create quantity unit"
@@ -234,6 +243,9 @@ msgstr "Edit product"
msgid "Edit location" msgid "Edit location"
msgstr "Edit location" msgstr "Edit location"
msgid "Edit shopping location"
msgstr "Edit shopping location"
msgid "Record data" msgid "Record data"
msgstr "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\"?" msgid "Are you sure to delete location \"%s\"?"
msgstr "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" msgid "Manage API keys"
msgstr "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" msgid "You have to select a location"
msgstr "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" msgid "List"
msgstr "List" msgstr "List"

View File

@@ -99,6 +99,9 @@ msgstr "Suivi des piles"
msgid "Locations" msgid "Locations"
msgstr "Emplacements" msgstr "Emplacements"
msgid "Shopping locations"
msgstr "Commerces"
msgid "Quantity units" msgid "Quantity units"
msgstr "Formats" msgstr "Formats"
@@ -198,6 +201,9 @@ msgstr "Nom"
msgid "Location" msgid "Location"
msgstr "Emplacement" msgstr "Emplacement"
msgid "Shopping location"
msgstr "Commerce"
msgid "Min. stock amount" msgid "Min. stock amount"
msgstr "Quantité minimum en stock" msgstr "Quantité minimum en stock"
@@ -237,6 +243,9 @@ msgstr "Facteur entre la quantité à l'achat et la quantité en stock"
msgid "Create location" msgid "Create location"
msgstr "Créer un emplacement" msgstr "Créer un emplacement"
msgid "Create shopping location"
msgstr "Créer un commerce"
msgid "Create quantity unit" msgid "Create quantity unit"
msgstr "Créer un format" msgstr "Créer un format"
@@ -270,6 +279,9 @@ msgstr "Modifier le produit"
msgid "Edit location" msgid "Edit location"
msgstr "Modifier l'emplacement" msgstr "Modifier l'emplacement"
msgid "Edit shopping location"
msgstr "Modifier le commerce"
msgid "Record data" msgid "Record data"
msgstr "Enregistrer les données" 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\"?" msgid "Are you sure to delete location \"%s\"?"
msgstr "Voulez-vous vraiment supprimer l'emplacement \"%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" msgid "Manage API keys"
msgstr "Gérer les clefs API" msgstr "Gérer les clefs API"
@@ -1124,6 +1139,9 @@ msgstr ""
msgid "You have to select a location" msgid "You have to select a location"
msgstr "Vous devez sélectionner un endroit" msgstr "Vous devez sélectionner un endroit"
msgid "You have to select a shopping location"
msgstr "Vous devez sélectionner un commerce"
msgid "List" msgid "List"
msgstr "Liste" msgstr "Liste"

View File

@@ -79,6 +79,9 @@ msgstr ""
msgid "Locations" msgid "Locations"
msgstr "" msgstr ""
msgid "Shopping locations"
msgstr ""
msgid "Quantity units" msgid "Quantity units"
msgstr "" msgstr ""
@@ -175,6 +178,9 @@ msgstr ""
msgid "Location" msgid "Location"
msgstr "" msgstr ""
msgid "Shopping location"
msgstr ""
msgid "Min. stock amount" msgid "Min. stock amount"
msgstr "" msgstr ""
@@ -214,6 +220,9 @@ msgstr ""
msgid "Create location" msgid "Create location"
msgstr "" msgstr ""
msgid "Create shopping location"
msgstr ""
msgid "Create quantity unit" msgid "Create quantity unit"
msgstr "" msgstr ""
@@ -247,6 +256,9 @@ msgstr ""
msgid "Edit location" msgid "Edit location"
msgstr "" msgstr ""
msgid "Edit shopping location"
msgstr ""
msgid "Record data" msgid "Record data"
msgstr "" msgstr ""
@@ -319,6 +331,9 @@ msgstr ""
msgid "Are you sure to delete location \"%s\"?" msgid "Are you sure to delete location \"%s\"?"
msgstr "" msgstr ""
msgid "Are you sure to delete shopping location \"%s\"?"
msgstr ""
msgid "Manage API keys" msgid "Manage API keys"
msgstr "" msgstr ""
@@ -1022,6 +1037,9 @@ msgstr ""
msgid "You have to select a location" msgid "You have to select a location"
msgstr "" msgstr ""
msgid "You have to select a shopping location"
msgstr ""
msgid "List" msgid "List"
msgstr "" msgstr ""

12
migrations/0099.sql Normal file
View File

@@ -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;

View File

@@ -118,12 +118,25 @@ Grocy.Components.ProductCard.Refresh = function(productId)
$("#productcard-no-price-data-hint").addClass("d-none"); $("#productcard-no-price-data-hint").addClass("d-none");
Grocy.Components.ProductCard.ReInitPriceHistoryChart(); Grocy.Components.ProductCard.ReInitPriceHistoryChart();
var datasets = {};
var chart = Grocy.Components.ProductCard.PriceHistoryChart.data;
priceHistoryDataPoints.forEach((dataPoint) => 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(); Grocy.Components.ProductCard.PriceHistoryChart.update();
} }
@@ -155,13 +168,9 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function()
labels: [ //Date objects labels: [ //Date objects
// Will be populated in Grocy.Components.ProductCard.Refresh // Will be populated in Grocy.Components.ProductCard.Refresh
], ],
datasets: [{ datasets: [ //Datasets
data: [ // Will be populated in Grocy.Components.ProductCard.Refresh
// Will be populated in Grocy.Components.ProductCard.Refresh ]
],
fill: false,
borderColor: '%s7a2b8'
}]
}, },
options: { options: {
scales: { scales: {
@@ -189,7 +198,7 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function()
}] }]
}, },
legend: { legend: {
display: false display: true
} }
} }
}); });

View File

@@ -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();
}

View File

@@ -17,6 +17,7 @@
var jsonData = { }; var jsonData = { };
jsonData.new_amount = jsonForm.new_amount; jsonData.new_amount = jsonForm.new_amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); 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) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
@@ -84,6 +85,7 @@
$('#price').val(''); $('#price').val('');
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ShoppingLocationPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');
@@ -150,6 +152,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
Grocy.Components.LocationPicker.SetId(productDetails.location.id); Grocy.Components.LocationPicker.SetId(productDetails.location.id);

View File

@@ -29,6 +29,7 @@
var jsonData = {}; var jsonData = {};
jsonData.amount = amount; jsonData.amount = amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue();
jsonData.price = price; jsonData.price = price;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
@@ -99,6 +100,7 @@
} }
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ShoppingLocationPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
@@ -138,6 +140,7 @@ if (Grocy.Components.ProductPicker !== undefined)
function(productDetails) function(productDetails)
{ {
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
Grocy.Components.LocationPicker.SetId(productDetails.location.id); Grocy.Components.LocationPicker.SetId(productDetails.location.id);

View File

@@ -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');

View File

@@ -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);
}
);
}
}
});
});

View File

@@ -14,6 +14,7 @@
jsonData.amount = jsonForm.amount; jsonData.amount = jsonForm.amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.purchased_date = Grocy.Components.DateTimePicker2.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) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); jsonData.location_id = Grocy.Components.LocationPicker.GetValue();

View File

@@ -57,6 +57,13 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting'); $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 // Shopping list routes
if (GROCY_FEATURE_FLAG_SHOPPINGLIST) if (GROCY_FEATURE_FLAG_SHOPPINGLIST)
{ {

View File

@@ -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); $averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days);
$lastPrice = null; $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(); $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)) if ($lastLogRow !== null && !empty($lastLogRow))
{ {
$lastPrice = $lastLogRow->price; $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; $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_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock, 'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice, 'last_price' => $lastPrice,
'last_shopping_location_id' => $lastShoppingLocation,
'next_best_before_date' => $nextBestBeforeDate, 'next_best_before_date' => $nextBestBeforeDate,
'location' => $location, 'location' => $location,
'average_shelf_life_days' => $averageShelfLifeDays, 'average_shelf_life_days' => $averageShelfLifeDays,
@@ -168,12 +171,14 @@ class StockService extends BaseService
} }
$returnData = array(); $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'); $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) foreach ($rows as $row)
{ {
$returnData[] = array( $returnData[] = array(
'date' => $row->purchased_date, 'date' => $row->purchased_date,
'price' => $row->price 'price' => $row->price,
'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id)->name,
); );
} }
return $returnData; return $returnData;
@@ -210,7 +215,7 @@ class StockService extends BaseService
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); 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)) if (!$this->ProductExists($productId))
{ {
@@ -266,7 +271,8 @@ class StockService extends BaseService
'transaction_type' => $transactionType, 'transaction_type' => $transactionType,
'price' => $price, 'price' => $price,
'location_id' => $locationId, 'location_id' => $locationId,
'transaction_id' => $transactionId 'transaction_id' => $transactionId,
'shopping_location_id' => $shoppingLocationId,
)); ));
$logRow->save(); $logRow->save();
@@ -279,7 +285,8 @@ class StockService extends BaseService
'purchased_date' => $purchasedDate, 'purchased_date' => $purchasedDate,
'stock_id' => $stockId, 'stock_id' => $stockId,
'price' => $price, 'price' => $price,
'location_id' => $locationId 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
)); ));
$stockRow->save(); $stockRow->save();
@@ -589,7 +596,7 @@ class StockService extends BaseService
return $this->getDatabase()->lastInsertId(); 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(); $stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch();
@@ -611,6 +618,7 @@ class StockService extends BaseService
'price' => $stockRow->price, 'price' => $stockRow->price,
'opened_date' => $stockRow->opened_date, 'opened_date' => $stockRow->opened_date,
'location_id' => $stockRow->location_id, 'location_id' => $stockRow->location_id,
'shopping_location_id' => $stockRow->shopping_location_id,
'correlation_id' => $correlationId, 'correlation_id' => $correlationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id 'stock_row_id' => $stockRow->id
@@ -632,6 +640,7 @@ class StockService extends BaseService
'price' => $price, 'price' => $price,
'best_before_date' => $bestBeforeDate, 'best_before_date' => $bestBeforeDate,
'location_id' => $locationId, 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'opened_date' => $openedDate, 'opened_date' => $openedDate,
'open' => $open, 'open' => $open,
'purchased_date' => $purchasedDate 'purchased_date' => $purchasedDate
@@ -647,6 +656,7 @@ class StockService extends BaseService
'price' => $price, 'price' => $price,
'opened_date' => $stockRow->opened_date, 'opened_date' => $stockRow->opened_date,
'location_id' => $locationId, 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'correlation_id' => $correlationId, 'correlation_id' => $correlationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id 'stock_row_id' => $stockRow->id
@@ -656,7 +666,7 @@ class StockService extends BaseService
return $this->getDatabase()->lastInsertId(); 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)) if (!$this->ProductExists($productId))
{ {
@@ -670,6 +680,11 @@ class StockService extends BaseService
$price = $productDetails->last_price; $price = $productDetails->last_price;
} }
if ($shoppingLocationId === null)
{
$shoppingLocationId = $productDetails->last_shopping_location_id;
}
// Tare weight handling // Tare weight handling
// The given amount is the new total amount including the container weight (gross) // 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 // 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; $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) else if ($newAmount < $productDetails->stock_amount + $containerWeight)
{ {

View File

@@ -0,0 +1,20 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/shoppinglocationpicker.js', true) }}?v={{ $version }}"></script>
@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
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
<label for="shopping_location_id">{{ $__t('Shopping location') }}&nbsp;&nbsp;<span id="{{ $hintId }}" class="small text-muted">{{ $hint }}</span></label>
<select class="form-control shopping-location-combobox" id="shopping_location_id" name="shopping_location_id" @if($isRequired) required @endif>
<option value=""></option>
@foreach($shoppinglocations as $shoppinglocation)
<option value="{{ $shoppinglocation->id }}">{{ $shoppinglocation->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('You have to select a shopping location') }}</div>
</div>

View File

@@ -66,6 +66,10 @@
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
'isRequired' => false 'isRequired' => false
)) ))
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
))
@else @else
<input type="hidden" name="price" id="price" value="0"> <input type="hidden" name="price" id="price" value="0">
@endif @endif

View File

@@ -243,6 +243,14 @@
</a> </a>
</li> </li>
@endif @endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<li data-nav-for-page="shoppinglocations" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/shoppinglocations') }}">
<i class="fas fa-shopping-cart"></i>
<span class="nav-link-text">{{ $__t('Shopping locations') }}</span>
</a>
</li>
@endif
<li data-nav-for-page="quantityunits" data-sub-menu-of="#top-nav-manager-master-data"> <li data-nav-for-page="quantityunits" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}"> <a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}">
<i class="fas fa-balance-scale"></i> <i class="fas fa-balance-scale"></i>

View File

@@ -30,6 +30,7 @@
'nextInputSelector' => '#best_before_date .datetimepicker-input' 'nextInputSelector' => '#best_before_date .datetimepicker-input'
)) ))
@php @php
$additionalGroupCssClasses = ''; $additionalGroupCssClasses = '';
if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
@@ -84,6 +85,9 @@
<input class="form-check-input" type="radio" name="price-type" id="price-type-total-price" value="total-price"> <input class="form-check-input" type="radio" name="price-type" id="price-type-total-price" value="total-price">
<label class="form-check-label" for="price-type-total-price">{{ $__t('Total price') }}</label> <label class="form-check-label" for="price-type-total-price">{{ $__t('Total price') }}</label>
</div> </div>
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
))
@else @else
<input type="hidden" name="price" id="price" value="0"> <input type="hidden" name="price" id="price" value="0">
@endif @endif

View File

@@ -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')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $shoppinglocation->id }};</script>
@endif
<form id="shoppinglocation-form" novalidate>
<div class="form-group">
<label for="name">{{ $__t('Name') }}</label>
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $shoppinglocation->name }}@endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $shoppinglocation->description }}@endif</textarea>
</div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'shopping_locations'
))
<button id="save-shopping-location-button" class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@@ -0,0 +1,73 @@
@extends('layout.default')
@section('title', $__t('Shopping locations'))
@section('activeNav', 'shoppinglocations')
@section('viewJsName', 'shoppinglocations')
@section('content')
<div class="row">
<div class="col">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/shoppinglocation/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $__t('Add') }}
</a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=shoppinglocations') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $__t('Configure userfields') }}
</a>
</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $__t('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="shoppinglocations-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr>
</thead>
<tbody class="d-none">
@foreach($shoppinglocations as $shoppinglocation)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/shoppinglocation/') }}{{ $shoppinglocation->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm shoppinglocation-delete-button" href="#" data-shoppinglocation-id="{{ $shoppinglocation->id }}" data-shoppinglocation-name="{{ $shoppinglocation->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $shoppinglocation->name }}
</td>
<td>
{{ $shoppinglocation->description }}
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id)
))
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@@ -35,6 +35,7 @@
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th>{{ $__t('Best before date') }}</th> <th>{{ $__t('Best before date') }}</th>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif
<th>{{ $__t('Shopping location') }}</th>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<th>{{ $__t('Price') }}</th>@endif @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<th>{{ $__t('Price') }}</th>@endif
<th>{{ $__t('Purchased date') }}</th> <th>{{ $__t('Purchased date') }}</th>
@@ -145,6 +146,9 @@
<td id="stock-{{ $stockEntry->id }}-price" class="locale-number locale-number-currency" data-price-id="{{ $stockEntry->price }}"> <td id="stock-{{ $stockEntry->id }}-price" class="locale-number locale-number-currency" data-price-id="{{ $stockEntry->price }}">
{{ $stockEntry->price }} {{ $stockEntry->price }}
</td> </td>
<td id="stock-{{ $stockEntry->id }}-shopping-location" data-shopping-location-id="{{ $stockEntry->shopping_location_id }}">
{{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }}
</td>
@endif @endif
<td> <td>
<span id="stock-{{ $stockEntry->id }}-purchased-date">{{ $stockEntry->purchased_date }}</span> <span id="stock-{{ $stockEntry->id }}-purchased-date">{{ $stockEntry->purchased_date }}</span>

View File

@@ -65,6 +65,10 @@
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
'isRequired' => false 'isRequired' => false
)) ))
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
'prefillById' => $stockEntry->shopping_location_id
))
@else @else
<input type="hidden" name="price" id="price" value="0"> <input type="hidden" name="price" id="price" value="0">
@endif @endif