Merge branch 'master' into small-fixes

This commit is contained in:
Bernd Bestel 2020-03-25 20:26:39 +01:00 committed by GitHub
commit 325d2b89fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 618 additions and 38 deletions

View File

@ -1,6 +1,13 @@
### New feature: Price history per store
- Define stores under master data
- Track on purchase/inventory in which store you bought the product
- => The price history chart on the product card shows a line per store
- (Thanks @immae)
### Recipe fixes ### Recipe fixes
- Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel)
### General & other improvements ### General & other improvements
- Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu)
- Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-) - Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-)
- Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto)

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 store 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 store 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 store 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": {
"$ref": "#/components/schemas/ShoppingLocation"
} }
} }
}, },

View File

@ -138,6 +138,20 @@ function BoolToString(bool $bool)
return $bool ? 'true' : 'false'; return $bool ? 'true' : 'false';
} }
function ExternalSettingValue(string $value)
{
$tvalue = rtrim($value, "\r\n");
$lvalue = strtolower($tvalue);
if ($lvalue === "true"){
return true;
}
elseif ($lvalue === "false")
{
return false;
}
return $tvalue;
}
function Setting(string $name, $value) function Setting(string $name, $value)
{ {
if (!defined('GROCY_' . $name)) if (!defined('GROCY_' . $name))
@ -146,22 +160,11 @@ function Setting(string $name, $value)
$settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt'; $settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt';
if (file_exists($settingOverrideFile)) if (file_exists($settingOverrideFile))
{ {
define('GROCY_' . $name, file_get_contents($settingOverrideFile)); define('GROCY_' . $name, ExternalSettingValue(file_get_contents($settingOverrideFile)));
} }
elseif (getenv('GROCY_' . $name) !== false) // An environment variable with the same name and prefix GROCY_ overwrites the given setting elseif (getenv('GROCY_' . $name) !== false) // An environment variable with the same name and prefix GROCY_ overwrites the given setting
{ {
if (strtolower(getenv('GROCY_' . $name)) === "true") define('GROCY_' . $name, ExternalSettingValue(getenv('GROCY_'. $name)));
{
define('GROCY_' . $name, true);
}
elseif (strtolower(getenv('GROCY_' . $name)) === "false")
{
define('GROCY_' . $name, false);
}
else
{
define('GROCY_' . $name, getenv('GROCY_' . $name));
}
} }
else else
{ {

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"

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,30 @@ 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 = __t("Unknown store");
if (dataPoint.shopping_location)
{
key = dataPoint.shopping_location.name
}
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 +173,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 +203,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 store "%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

@ -166,18 +166,35 @@ function RefreshStockEntryRow(stockRowId)
function(locationResult) function(locationResult)
{ {
locationName = locationResult.name; locationName = locationResult.name;
$('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id);
$('#stock-' + stockRowId + '-location').text(locationName);
}, },
function(xhr) function(xhr)
{ {
console.error(xhr); console.error(xhr);
} }
); );
$('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id);
$('#stock-' + stockRowId + '-location').text(locationName);
$('#stock-' + stockRowId + '-price').text(result.price); $('#stock-' + stockRowId + '-price').text(result.price);
$('#stock-' + stockRowId + '-purchased-date').text(result.purchased_date); $('#stock-' + stockRowId + '-purchased-date').text(result.purchased_date);
$('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59'); $('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59');
var shoppingLocationName = "";
Grocy.Api.Get("objects/shopping_locations/" + result.shopping_location_id,
function(shoppingLocationResult)
{
shoppingLocationName = shoppingLocationResult.name;
$('#stock-' + stockRowId + '-shopping-location').attr('data-shopping-location-id', result.location_id);
$('#stock-' + stockRowId + '-shopping-location').text(shoppingLocationName);
},
function (xhr)
{
console.error(xhr);
}
);
if (result.open == 1) if (result.open == 1)
{ {
$('#stock-' + stockRowId + '-opened-amount').text(__t('Opened')); $('#stock-' + stockRowId + '-opened-amount').text(__t('Opened'));

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),
); );
} }
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('Store') }}&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 store') }}</div>
</div>

View File

@ -67,6 +67,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('Stores') }}</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)
@ -85,6 +86,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 store'))
@else
@section('title', $__t('Create store'))
@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('Stores'))
@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,7 +35,10 @@
<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
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<th>{{ $__t('Price') }}</th>@endif @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<th>{{ $__t('Store') }}</th>
<th>{{ $__t('Price') }}</th>
@endif
<th>{{ $__t('Purchased date') }}</th> <th>{{ $__t('Purchased date') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', array(
@ -142,6 +145,11 @@
</td> </td>
@endif @endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<td id="stock-{{ $stockEntry->id }}-shopping-location" data-shopping-location-id="{{ $stockEntry->shopping_location_id }}">
@if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id) !== null)
{{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }}
@endif
</td>
<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>

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,
'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