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'];
}
$shoppingLocationId = null;
if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id']))
{
$shoppingLocationId = $requestBody['shopping_location_id'];
}
$transactionType = StockService::TRANSACTION_TYPE_PURCHASE;
if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype']))
{
$transactionType = $requestBody['transactiontype'];
}
$bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId);
$bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId, $shoppingLocationId);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)
@@ -144,7 +150,13 @@ class StockApiController extends BaseApiController
$locationId = $requestBody['location_id'];
}
$bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $price, $requestBody['open'], $requestBody['purchased_date']);
$shoppingLocationId = null;
if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id']))
{
$shoppingLocationId = $requestBody['shopping_location_id'];
}
$bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date']);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)
@@ -312,7 +324,13 @@ class StockApiController extends BaseApiController
$price = $requestBody['price'];
}
$bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price);
$shoppingLocationId = null;
if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id']))
{
$shoppingLocationId = $requestBody['shopping_location_id'];
}
$bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price, $shoppingLocationId);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)

View File

@@ -38,6 +38,7 @@ class StockController extends BaseController
'products' => $this->getDatabase()->products()->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'),
'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(),
'nextXDays' => $nextXDays,
@@ -50,6 +51,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'purchase', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
}
@@ -76,6 +78,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'inventory', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
}
@@ -85,6 +88,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'stockentryform', [
'stockEntry' => $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(),
'products' => $this->getDatabase()->products()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
}
@@ -140,6 +144,15 @@ class StockController extends BaseController
]);
}
public function ShoppingLocationsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'shoppinglocations', [
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('shopping_locations')
]);
}
public function ProductGroupsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'productgroups', [
@@ -210,6 +223,25 @@ class StockController extends BaseController
}
}
public function ShoppingLocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if ($args['shoppingLocationId'] == 'new')
{
return $this->renderPage($response, 'shoppinglocationform', [
'mode' => 'create',
'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations')
]);
}
else
{
return $this->renderPage($response, 'shoppinglocationform', [
'shoppinglocation' => $this->getDatabase()->shopping_locations($args['shoppingLocationId']),
'mode' => 'edit',
'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations')
]);
}
}
public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if ($args['productGroupId'] == 'new')

View File

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

View File

@@ -66,6 +66,9 @@ msgstr "Products"
msgid "Locations"
msgstr "Locations"
msgid "Shopping locations"
msgstr "Shopping locations"
msgid "Quantity units"
msgstr "Quantity units"
@@ -162,6 +165,9 @@ msgstr "Name"
msgid "Location"
msgstr "Location"
msgid "Shopping location"
msgstr "Shopping location"
msgid "Min. stock amount"
msgstr "Min. stock amount"
@@ -201,6 +207,9 @@ msgstr "Factor purchase to stock quantity unit"
msgid "Create location"
msgstr "Create location"
msgid "Create shopping location"
msgstr "Create shopping location"
msgid "Create quantity unit"
msgstr "Create quantity unit"
@@ -234,6 +243,9 @@ msgstr "Edit product"
msgid "Edit location"
msgstr "Edit location"
msgid "Edit shopping location"
msgstr "Edit shopping location"
msgid "Record data"
msgstr "Record data"
@@ -306,6 +318,9 @@ msgstr "Are you sure to delete product \"%s\"?"
msgid "Are you sure to delete location \"%s\"?"
msgstr "Are you sure to delete location \"%s\"?"
msgid "Are you sure to delete shopping location \"%s\"?"
msgstr "Are you sure to delete shopping location \"%s\"?"
msgid "Manage API keys"
msgstr "Manage API keys"
@@ -1035,6 +1050,9 @@ msgstr "Tare weight handling enabled - please weigh the whole container, the amo
msgid "You have to select a location"
msgstr "You have to select a location"
msgid "You have to select a shopping location"
msgstr "You have to select a shopping location"
msgid "List"
msgstr "List"

View File

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

View File

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

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");
Grocy.Components.ProductCard.ReInitPriceHistoryChart();
var datasets = {};
var chart = Grocy.Components.ProductCard.PriceHistoryChart.data;
priceHistoryDataPoints.forEach((dataPoint) =>
{
Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate());
var key = dataPoint.shopping_location || "empty";
if (!datasets[key]) {
datasets[key] = []
}
chart.labels.push(moment(dataPoint.date).toDate());
datasets[key].push(dataPoint.price);
var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0];
dataset.data.push(dataPoint.price);
});
Object.keys(datasets).forEach((key) => {
chart.datasets.push({
data: datasets[key],
fill: false,
borderColor: "HSL(" + (129 * chart.datasets.length) + ",100%,50%)",
label: key,
});
});
Grocy.Components.ProductCard.PriceHistoryChart.update();
}
@@ -155,13 +168,9 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function()
labels: [ //Date objects
// Will be populated in Grocy.Components.ProductCard.Refresh
],
datasets: [{
data: [
// Will be populated in Grocy.Components.ProductCard.Refresh
],
fill: false,
borderColor: '%s7a2b8'
}]
datasets: [ //Datasets
// Will be populated in Grocy.Components.ProductCard.Refresh
]
},
options: {
scales: {
@@ -189,7 +198,7 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function()
}]
},
legend: {
display: false
display: true
}
}
});

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

View File

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

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.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue();
jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue();

View File

@@ -57,6 +57,13 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting');
}
// Stock price tracking
if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
$group->get('/shoppinglocations', '\Grocy\Controllers\StockController:ShoppingLocationsList');
$group->get('/shoppinglocation/{shoppingLocationId}', '\Grocy\Controllers\StockController:ShoppingLocationEditForm');
}
// Shopping list routes
if (GROCY_FEATURE_FLAG_SHOPPINGLIST)
{

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);
$lastPrice = null;
$lastShoppingLocation = null;
$lastLogRow = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch();
if ($lastLogRow !== null && !empty($lastLogRow))
{
$lastPrice = $lastLogRow->price;
$lastShoppingLocation = $lastLogRow->shopping_location_id;
}
$consumeCount = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 0')->sum('amount') * -1;
@@ -152,6 +154,7 @@ class StockService extends BaseService
'quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
'last_shopping_location_id' => $lastShoppingLocation,
'next_best_before_date' => $nextBestBeforeDate,
'location' => $location,
'average_shelf_life_days' => $averageShelfLifeDays,
@@ -168,12 +171,14 @@ class StockService extends BaseService
}
$returnData = array();
$shoppingLocations = $this->getDatabase()->shopping_locations();
$rows = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->whereNOT('price', null)->orderBy('purchased_date', 'DESC');
foreach ($rows as $row)
{
$returnData[] = array(
'date' => $row->purchased_date,
'price' => $row->price
'price' => $row->price,
'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id)->name,
);
}
return $returnData;
@@ -210,7 +215,7 @@ class StockService extends BaseService
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId);
}
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, &$transactionId = null)
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null)
{
if (!$this->ProductExists($productId))
{
@@ -266,7 +271,8 @@ class StockService extends BaseService
'transaction_type' => $transactionType,
'price' => $price,
'location_id' => $locationId,
'transaction_id' => $transactionId
'transaction_id' => $transactionId,
'shopping_location_id' => $shoppingLocationId,
));
$logRow->save();
@@ -279,7 +285,8 @@ class StockService extends BaseService
'purchased_date' => $purchasedDate,
'stock_id' => $stockId,
'price' => $price,
'location_id' => $locationId
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
));
$stockRow->save();
@@ -589,7 +596,7 @@ class StockService extends BaseService
return $this->getDatabase()->lastInsertId();
}
public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $price, $open, $purchasedDate)
public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate)
{
$stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch();
@@ -611,6 +618,7 @@ class StockService extends BaseService
'price' => $stockRow->price,
'opened_date' => $stockRow->opened_date,
'location_id' => $stockRow->location_id,
'shopping_location_id' => $stockRow->shopping_location_id,
'correlation_id' => $correlationId,
'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id
@@ -632,6 +640,7 @@ class StockService extends BaseService
'price' => $price,
'best_before_date' => $bestBeforeDate,
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'opened_date' => $openedDate,
'open' => $open,
'purchased_date' => $purchasedDate
@@ -647,6 +656,7 @@ class StockService extends BaseService
'price' => $price,
'opened_date' => $stockRow->opened_date,
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'correlation_id' => $correlationId,
'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id
@@ -656,7 +666,7 @@ class StockService extends BaseService
return $this->getDatabase()->lastInsertId();
}
public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null)
public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null, $shoppingLocationId = null)
{
if (!$this->ProductExists($productId))
{
@@ -670,6 +680,11 @@ class StockService extends BaseService
$price = $productDetails->last_price;
}
if ($shoppingLocationId === null)
{
$shoppingLocationId = $productDetails->last_shopping_location_id;
}
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// So assume that the amount in stock is the amount also including the container weight
@@ -691,7 +706,7 @@ class StockService extends BaseService
$bookingAmount = $newAmount;
}
return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId);
return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId, $shoppingLocationId);
}
else if ($newAmount < $productDetails->stock_amount + $containerWeight)
{

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'),
'isRequired' => false
))
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
))
@else
<input type="hidden" name="price" id="price" value="0">
@endif

View File

@@ -243,6 +243,14 @@
</a>
</li>
@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">
<a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}">
<i class="fas fa-balance-scale"></i>

View File

@@ -30,6 +30,7 @@
'nextInputSelector' => '#best_before_date .datetimepicker-input'
))
@php
$additionalGroupCssClasses = '';
if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
@@ -84,6 +85,9 @@
<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>
</div>
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
))
@else
<input type="hidden" name="price" id="price" value="0">
@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('Best before date') }}</th>
@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
<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 }}">
{{ $stockEntry->price }}
</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
<td>
<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'),
'isRequired' => false
))
@include('components.shoppinglocationpicker', array(
'shoppinglocations' => $shoppinglocations,
'prefillById' => $stockEntry->shopping_location_id
))
@else
<input type="hidden" name="price" id="price" value="0">
@endif