Implemented notes and Userfields for stock entries (closes #443)

This commit is contained in:
Bernd Bestel 2022-03-30 17:32:53 +02:00
parent 2983687f34
commit d3a39270de
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
18 changed files with 438 additions and 107 deletions

View File

@ -2,6 +2,17 @@
> ❗ xxxImportant upgrade informationXXX > ❗ xxxImportant upgrade informationXXX
### New feature: Notes and Userfields for stock entries
- Stock entries can now have notes
- For example to distinguish between same, yet different products (e.g. having only a generic product "Chocolate" and note in that field what special one it is exactly this time)
- => New field on the purchase and inventory page
- => New column on the stock entries and stock journal page
- => Visible also in the "Use a specific stock item" dropdown on the consume and transfer page
- Additionally it's also possible to add arbitrary own fields by using Userfields
- => Configure the desired Userfields for the entity `stock`
- => Those Userfields are then visible on the same places as mentioned above for the built-in "Note" field
### New feature: Recipes "Due score" ### New feature: Recipes "Due score"
- A number (new column on the recipes page) which represents a score which is higher the more ingredients, of the corresponding recipe, currently in stock are due soon, overdue or already expired - A number (new column on the recipes page) which represents a score which is higher the more ingredients, of the corresponding recipe, currently in stock are due soon, overdue or already expired

View File

@ -100,42 +100,36 @@ class StockApiController extends BaseApiController
} }
$bestBeforeDate = null; $bestBeforeDate = null;
if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date'])) if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date']))
{ {
$bestBeforeDate = $requestBody['best_before_date']; $bestBeforeDate = $requestBody['best_before_date'];
} }
$purchasedDate = date('Y-m-d'); $purchasedDate = date('Y-m-d');
if (array_key_exists('purchased_date', $requestBody) && IsIsoDate($requestBody['purchased_date'])) if (array_key_exists('purchased_date', $requestBody) && IsIsoDate($requestBody['purchased_date']))
{ {
$purchasedDate = $requestBody['purchased_date']; $purchasedDate = $requestBody['purchased_date'];
} }
$price = null; $price = null;
if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price'])) if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price']))
{ {
$price = $requestBody['price']; $price = $requestBody['price'];
} }
$locationId = null; $locationId = null;
if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id'])) if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id']))
{ {
$locationId = $requestBody['location_id']; $locationId = $requestBody['location_id'];
} }
$shoppingLocationId = null; $shoppingLocationId = null;
if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id']))
{ {
$shoppingLocationId = $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'];
@ -147,7 +141,13 @@ class StockApiController extends BaseApiController
$stockLabelType = intval($requestBody['stock_label_type']); $stockLabelType = intval($requestBody['stock_label_type']);
} }
$transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $stockLabelType); $note = null;
if (array_key_exists('note', $requestBody))
{
$note = $requestBody['note'];
}
$transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $stockLabelType, false, $note);
$args['transactionId'] = $transactionId; $args['transactionId'] = $transactionId;
return $this->StockTransactions($request, $response, $args); return $this->StockTransactions($request, $response, $args);
@ -394,34 +394,36 @@ class StockApiController extends BaseApiController
} }
$bestBeforeDate = null; $bestBeforeDate = null;
if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date'])) if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date']))
{ {
$bestBeforeDate = $requestBody['best_before_date']; $bestBeforeDate = $requestBody['best_before_date'];
} }
$price = null; $price = null;
if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price'])) if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price']))
{ {
$price = $requestBody['price']; $price = $requestBody['price'];
} }
$locationId = null; $locationId = null;
if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id'])) if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id']))
{ {
$locationId = $requestBody['location_id']; $locationId = $requestBody['location_id'];
} }
$shoppingLocationId = null; $shoppingLocationId = null;
if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id']))
{ {
$shoppingLocationId = $requestBody['shopping_location_id']; $shoppingLocationId = $requestBody['shopping_location_id'];
} }
$transactionId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date']); $note = null;
if (array_key_exists('note', $requestBody))
{
$note = $requestBody['note'];
}
$transactionId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date'], $note);
$args['transactionId'] = $transactionId; $args['transactionId'] = $transactionId;
return $this->StockTransactions($request, $response, $args); return $this->StockTransactions($request, $response, $args);
} }
@ -506,7 +508,13 @@ class StockApiController extends BaseApiController
$stockLabelType = intval($requestBody['stock_label_type']); $stockLabelType = intval($requestBody['stock_label_type']);
} }
$transactionId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price, $shoppingLocationId, $purchasedDate, $stockLabelType); $note = null;
if (array_key_exists('note', $requestBody))
{
$note = $requestBody['note'];
}
$transactionId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price, $shoppingLocationId, $purchasedDate, $stockLabelType, $note);
$args['transactionId'] = $transactionId; $args['transactionId'] = $transactionId;
return $this->StockTransactions($request, $response, $args); return $this->StockTransactions($request, $response, $args);
} }

View File

@ -29,7 +29,8 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved() 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('stock')
]); ]);
} }
@ -59,7 +60,9 @@ class StockController extends BaseController
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'users' => $usersService->GetUsersAsDto(), 'users' => $usersService->GetUsersAsDto(),
'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_') 'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_'),
'userfieldsStock' => $this->getUserfieldsService()->GetFields('stock'),
'userfieldValuesStock' => $this->getUserfieldsService()->GetAllValues('stock')
]); ]);
} }
@ -261,7 +264,8 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved() 'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('stock')
]); ]);
} }
@ -498,8 +502,10 @@ class StockController extends BaseController
'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,
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfieldsProducts' => $this->getUserfieldsService()->GetFields('products'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('products') 'userfieldValuesProducts' => $this->getUserfieldsService()->GetAllValues('products'),
'userfieldsStock' => $this->getUserfieldsService()->GetFields('stock'),
'userfieldValuesStock' => $this->getUserfieldsService()->GetAllValues('stock')
]); ]);
} }

View File

@ -1911,6 +1911,10 @@
"stock_label_type": { "stock_label_type": {
"type": "integer", "type": "integer",
"description": "`1` = No label, `2` = Single label, `3` = Label per unit" "description": "`1` = No label, `2` = Single label, `3` = Label per unit"
},
"note": {
"type": "string",
"description": "An optional note for the corresponding stock entry"
} }
}, },
"example": { "example": {
@ -2167,6 +2171,10 @@
"stock_label_type": { "stock_label_type": {
"type": "integer", "type": "integer",
"description": "`1` = No label, `2` = Single label, `3` = Label per unit (only applies to added products)" "description": "`1` = No label, `2` = Single label, `3` = Label per unit (only applies to added products)"
},
"note": {
"type": "string",
"description": "An optional note for the corresponding stock entry (only applies to added products)"
} }
} }
} }
@ -4659,6 +4667,9 @@
"type": "string", "type": "string",
"format": "date" "format": "date"
}, },
"note": {
"type": "string"
},
"row_created_timestamp": { "row_created_timestamp": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@ -5286,6 +5297,9 @@
"transaction_type": { "transaction_type": {
"$ref": "#/components/schemas/StockTransactionType" "$ref": "#/components/schemas/StockTransactionType"
}, },
"note": {
"type": "string"
},
"row_created_timestamp": { "row_created_timestamp": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"

100
migrations/0178.sql Normal file
View File

@ -0,0 +1,100 @@
ALTER TABLE stock
ADD note TEXT;
ALTER TABLE stock_log
ADD note TEXT;
PRAGMA legacy_alter_table = ON;
ALTER TABLE userfield_values RENAME TO userfield_values_old;
CREATE TABLE userfield_values (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
field_id INTEGER NOT NULL,
object_id TEXT NOT NULL,
value TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(field_id, object_id)
);
INSERT INTO userfield_values
(id, field_id, object_id, value, row_created_timestamp)
SELECT id, field_id, object_id, value, row_created_timestamp
FROM userfield_values_old;
DROP TABLE userfield_values_old;
CREATE TRIGGER userfield_values_special_handling AFTER INSERT ON userfield_values
BEGIN
-- Entity stock:
-- object_id is the transaction_id on insert -> replace it by the corresponding stock_id
INSERT OR REPLACE INTO userfield_values
(field_id, object_id, value)
SELECT uv.field_id, sl.stock_id, uv.value
FROM userfield_values uv
JOIN stock_log sl
ON uv.object_id = sl.transaction_id
AND sl.transaction_type IN ('purchase', 'inventory-correction')
WHERE uv.field_id IN (SELECT id FROM userfields WHERE entity = 'stock')
AND uv.field_id = NEW.field_id
AND uv.object_id = NEW.object_id;
DELETE FROM userfield_values
WHERE field_id IN (SELECT id FROM userfields WHERE entity = 'stock')
AND field_id = NEW.field_id
AND object_id = NEW.object_id;
END;
DROP VIEW stock_splits;
CREATE VIEW stock_splits
AS
/*
Helper view which shows splitted stock rows which could be compacted
(a stock_id starting with "x" indicates that this entry shouldn't be compacted)
*/
SELECT
product_id,
SUM(amount) AS total_amount,
MIN(stock_id) AS stock_id_to_keep,
MAX(id) AS id_to_keep,
GROUP_CONCAT(id) AS id_group,
GROUP_CONCAT(stock_id) AS stock_id_group,
id -- Dummy
FROM stock
WHERE stock_id NOT LIKE 'x%'
GROUP BY product_id, best_before_date, purchased_date, price, open, opened_date, location_id, shopping_location_id, IFNULL(note, '')
HAVING COUNT(*) > 1;
DROP VIEW uihelper_stock_journal;
CREATE VIEW uihelper_stock_journal
AS
SELECT
sl.id,
sl.row_created_timestamp,
sl.correlation_id,
sl.undone,
sl.undone_timestamp,
sl.transaction_type,
sl.spoiled,
sl.amount,
sl.location_id,
l.name AS location_name,
p.name AS product_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
u.display_name AS user_display_name,
p.id AS product_id,
sl.note,
sl.stock_id
FROM stock_log sl
LEFT JOIN users_dto u
ON sl.user_id = u.id
JOIN products p
ON sl.product_id = p.id
JOIN locations l
ON sl.location_id = l.id
JOIN quantity_units qu
ON p.qu_id_stock = qu.id;

View File

@ -171,6 +171,80 @@ Grocy.Components.UserfieldsForm.Load = function()
); );
} }
Grocy.Components.UserfieldsForm.Clear = function()
{
if (!$("#userfields-form").length)
{
return;
}
Grocy.Api.Get('objects/userfields?query[]=entity=' + $("#userfields-form").data("entity"),
function(result)
{
$.each(result, function(key, userfield)
{
var input = $(".userfield-input[data-userfield-name='" + userfield.name + "']");
if (input.attr("type") == "checkbox")
{
input.prop("checked", false);
}
else if (input.hasAttr("multiple"))
{
input.val("");
$(".selectpicker").selectpicker("render");
}
else if (input.attr('type') == "file")
{
var formGroup = input.parent().parent().parent();
formGroup.find("label.custom-file-label").text("");
formGroup.find(".userfield-file-show").attr('href', U('/files/userfiles/' + value));
formGroup.find('.userfield-file-show').removeClass('d-none');
formGroup.find('img.userfield-current-file')
.attr('src', U('/files/userfiles/' + value + '?force_serve_as=picture&best_fit_width=250&best_fit_height=250'));
LoadImagesLazy();
formGroup.find('.userfield-file-delete').click(
function()
{
formGroup.find("label.custom-file-label").text(__t("No file selected"));
formGroup.find(".userfield-file-show").addClass('d-none');
input.attr('data-old-file', "");
}
);
input.on("change", function(e)
{
formGroup.find(".userfield-file-show").addClass('d-none');
});
}
else if (input.attr("data-userfield-type") == "link")
{
var formRow = input.parent().parent();
formRow.find(".userfield-link-title").val(data.title);
formRow.find(".userfield-link-link").val(data.link);
input.val("");
}
else
{
input.val("");
}
});
$("form").each(function()
{
Grocy.FrontendHelpers.ValidateForm(this.id);
});
},
function(xhr)
{
console.error(xhr);
}
);
}
$(".userfield-link").keyup(function(e) $(".userfield-link").keyup(function(e)
{ {
var formRow = $(this).parent().parent(); var formRow = $(this).parent().parent();

View File

@ -286,10 +286,16 @@ function OnLocationChange(locationId, stockId)
{ {
if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0) if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0)
{ {
var noteTxt = "";
if (stockEntry.note != null && !stockEntry.note.isEmpty())
{
noteTxt = " " + stockEntry.note;
}
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt + noteTxt
})); }));
} }

View File

@ -27,6 +27,7 @@
var jsonData = {}; var jsonData = {};
jsonData.new_amount = jsonForm.amount; jsonData.new_amount = jsonForm.amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.note = jsonForm.note;
jsonData.stock_label_type = jsonForm.stock_label_type; jsonData.stock_label_type = jsonForm.stock_label_type;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
@ -116,6 +117,7 @@
} }
} }
Grocy.EditObjectId = result[0].transaction_id;
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(result) function(result)
{ {
@ -123,39 +125,48 @@
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined)
{ {
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); Grocy.Components.UserfieldsForm.Save(function()
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); {
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
});
} }
else else
{ {
Grocy.FrontendHelpers.EndUiBusy("inventory-form"); Grocy.Components.UserfieldsForm.Save(function()
toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow();
Grocy.Components.ProductAmountPicker.Reset();
$('#inventory-change-info').addClass('d-none');
$("#tare-weight-handling-info").addClass("d-none");
$("#display_amount").attr("min", "0");
$('#display_amount').val('');
$('#display_amount').removeAttr("data-not-equal");
$(".input-group-productamountpicker").trigger("change");
$('#price').val('');
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.FrontendHelpers.EndUiBusy("inventory-form");
} toastr.success(successMessage);
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.FinishFlow();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) Grocy.Components.ProductAmountPicker.Reset();
{ $('#inventory-change-info').addClass('d-none');
$("#stock_label_type").val(0); $("#tare-weight-handling-info").addClass("d-none");
} $("#display_amount").attr("min", "0");
$('#display_amount').val('');
$('#display_amount').removeAttr("data-not-equal");
$(".input-group-productamountpicker").trigger("change");
$('#price').val('');
$('#note').val('');
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
Grocy.Components.ShoppingLocationPicker.SetValue('');
}
Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.Components.UserfieldsForm.Clear();
Grocy.FrontendHelpers.ValidateForm('inventory-form'); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER)
{
$("#stock_label_type").val(0);
}
Grocy.FrontendHelpers.ValidateForm('inventory-form');
});
} }
}, },
function(xhr) function(xhr)

View File

@ -28,6 +28,7 @@ $('#save-purchase-button').on('click', function(e)
{ {
var jsonData = {}; var jsonData = {};
jsonData.amount = jsonForm.amount; jsonData.amount = jsonForm.amount;
jsonData.note = jsonForm.note;
jsonData.stock_label_type = jsonForm.stock_label_type; jsonData.stock_label_type = jsonForm.stock_label_type;
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@ -165,61 +166,70 @@ $('#save-purchase-button').on('click', function(e)
} }
} }
Grocy.EditObjectId = result[0].transaction_id;
if (GetUriParam("embedded") !== undefined) if (GetUriParam("embedded") !== undefined)
{ {
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); Grocy.Components.UserfieldsForm.Save(function()
window.parent.postMessage(WindowMessageBag("AfterItemAdded", GetUriParam("listitemid")), Grocy.BaseUrl); {
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("AfterItemAdded", GetUriParam("listitemid")), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
});
} }
else else
{ {
Grocy.FrontendHelpers.EndUiBusy("purchase-form"); Grocy.Components.UserfieldsForm.Save(function()
toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next))
{ {
if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_due_date)) Grocy.FrontendHelpers.EndUiBusy("purchase-form");
toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next))
{ {
toastr.warning(__t("This is due earlier than already in-stock items")); if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_due_date))
{
toastr.warning(__t("This is due earlier than already in-stock items"));
}
} }
}
Grocy.Components.ProductAmountPicker.Reset(); Grocy.Components.ProductAmountPicker.Reset();
$("#purchase-form").removeAttr("data-used-barcode"); $("#purchase-form").removeAttr("data-used-barcode");
$("#display_amount").attr("min", Grocy.DefaultMinAmount); $("#display_amount").attr("min", Grocy.DefaultMinAmount);
$('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount)); $('#display_amount').val(parseFloat(Grocy.UserSettings.stock_default_purchase_amount));
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
$('#price').val(''); $('#price').val('');
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{ {
Grocy.Components.LocationPicker.Clear(); Grocy.Components.LocationPicker.Clear();
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{ {
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
} }
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
Grocy.Components.ShoppingLocationPicker.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);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER)
{ {
$("#stock_label_type").val(0); $("#stock_label_type").val(0);
} }
$('#price-hint').text(""); $('#price-hint').text("");
var priceTypeUnitPrice = $("#price-type-unit-price"); $('#note').val("");
var priceTypeUnitPriceLabel = $("[for=" + priceTypeUnitPrice.attr("id") + "]"); var priceTypeUnitPrice = $("#price-type-unit-price");
priceTypeUnitPriceLabel.text(__t("Unit price")); var priceTypeUnitPriceLabel = $("[for=" + priceTypeUnitPrice.attr("id") + "]");
priceTypeUnitPriceLabel.text(__t("Unit price"));
Grocy.Components.UserfieldsForm.Clear();
Grocy.FrontendHelpers.ValidateForm('purchase-form'); Grocy.FrontendHelpers.ValidateForm('purchase-form');
});
} }
}, },
function(xhr) function(xhr)

View File

@ -217,6 +217,7 @@ function RefreshStockEntryRow(stockRowId)
); );
$('#stock-' + stockRowId + '-price').text(result.price); $('#stock-' + stockRowId + '-price').text(result.price);
$('#stock-' + stockRowId + '-note').text(result.note);
$('#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');

View File

@ -24,6 +24,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.note = jsonForm.note;
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue();

View File

@ -315,10 +315,16 @@ $("#location_id_from").on('change', function(e)
{ {
if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0) if ($("#specific_stock_entry option[value='" + stockEntry.stock_id + "']").length == 0)
{ {
var noteTxt = "";
if (stockEntry.note != null && !stockEntry.note.isEmpty())
{
noteTxt = " " + stockEntry.note;
}
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt + noteTxt
})); }));
} }

View File

@ -114,7 +114,7 @@ class StockService extends BaseService
} }
} }
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null, $stockLabelType = 0, $addExactAmount = false) public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null, $stockLabelType = 0, $addExactAmount = false, $note = null)
{ {
if (!$this->ProductExists($productId)) if (!$this->ProductExists($productId))
{ {
@ -203,7 +203,8 @@ class StockService extends BaseService
'location_id' => $locationId, 'location_id' => $locationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'shopping_location_id' => $shoppingLocationId, 'shopping_location_id' => $shoppingLocationId,
'user_id' => GROCY_USER_ID 'user_id' => GROCY_USER_ID,
'note' => $note
]); ]);
$logRow->save(); $logRow->save();
@ -215,7 +216,8 @@ class StockService extends BaseService
'stock_id' => $stockId, 'stock_id' => $stockId,
'price' => $price, 'price' => $price,
'location_id' => $locationId, 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId 'shopping_location_id' => $shoppingLocationId,
'note' => $note
]); ]);
$stockRow->save(); $stockRow->save();
@ -252,7 +254,8 @@ class StockService extends BaseService
'location_id' => $locationId, 'location_id' => $locationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'shopping_location_id' => $shoppingLocationId, 'shopping_location_id' => $shoppingLocationId,
'user_id' => GROCY_USER_ID 'user_id' => GROCY_USER_ID,
'note' => $note
]); ]);
$logRow->save(); $logRow->save();
@ -264,7 +267,8 @@ class StockService extends BaseService
'stock_id' => $stockId, 'stock_id' => $stockId,
'price' => $price, 'price' => $price,
'location_id' => $locationId, 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId 'shopping_location_id' => $shoppingLocationId,
'note' => $note
]); ]);
$stockRow->save(); $stockRow->save();
@ -451,7 +455,8 @@ class StockService extends BaseService
'recipe_id' => $recipeId, 'recipe_id' => $recipeId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'user_id' => GROCY_USER_ID, 'user_id' => GROCY_USER_ID,
'location_id' => $stockEntry->location_id 'location_id' => $stockEntry->location_id,
'note' => $stockEntry->note
]); ]);
$logRow->save(); $logRow->save();
@ -478,7 +483,8 @@ class StockService extends BaseService
'recipe_id' => $recipeId, 'recipe_id' => $recipeId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'user_id' => GROCY_USER_ID, 'user_id' => GROCY_USER_ID,
'location_id' => $stockEntry->location_id 'location_id' => $stockEntry->location_id,
'note' => $stockEntry->note
]); ]);
$logRow->save(); $logRow->save();
@ -500,7 +506,7 @@ class StockService extends BaseService
} }
} }
public function EditStockEntry(int $stockRowId, float $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate) public function EditStockEntry(int $stockRowId, float $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate, $note = null)
{ {
$stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch(); $stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch();
if ($stockRow === null) if ($stockRow === null)
@ -524,7 +530,8 @@ class StockService extends BaseService
'correlation_id' => $correlationId, 'correlation_id' => $correlationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id, 'stock_row_id' => $stockRow->id,
'user_id' => GROCY_USER_ID 'user_id' => GROCY_USER_ID,
'note' => $stockRow->note
]); ]);
$logOldRowForStockUpdate->save(); $logOldRowForStockUpdate->save();
@ -546,7 +553,8 @@ class StockService extends BaseService
'shopping_location_id' => $shoppingLocationId, 'shopping_location_id' => $shoppingLocationId,
'opened_date' => $openedDate, 'opened_date' => $openedDate,
'open' => BoolToInt($open), 'open' => BoolToInt($open),
'purchased_date' => $purchasedDate 'purchased_date' => $purchasedDate,
'note' => $note
]); ]);
$logNewRowForStockUpdate = $this->getDatabase()->stock_log()->createRow([ $logNewRowForStockUpdate = $this->getDatabase()->stock_log()->createRow([
@ -563,7 +571,8 @@ class StockService extends BaseService
'correlation_id' => $correlationId, 'correlation_id' => $correlationId,
'transaction_id' => $transactionId, 'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id, 'stock_row_id' => $stockRow->id,
'user_id' => GROCY_USER_ID 'user_id' => GROCY_USER_ID,
'note' => $stockRow->note
]); ]);
$logNewRowForStockUpdate->save(); $logNewRowForStockUpdate->save();
@ -852,7 +861,7 @@ class StockService extends BaseService
return $this->getDatabase()->stock()->where('id', $entryId)->fetch(); return $this->getDatabase()->stock()->where('id', $entryId)->fetch();
} }
public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null, $shoppingLocationId = null, $purchasedDate = null, $stockLabelType = 0) public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null, $shoppingLocationId = null, $purchasedDate = null, $stockLabelType = 0, $note = null)
{ {
if (!$this->ProductExists($productId)) if (!$this->ProductExists($productId))
{ {
@ -899,7 +908,7 @@ class StockService extends BaseService
$bookingAmount = $newAmount; $bookingAmount = $newAmount;
} }
return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $stockLabelType); return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $stockLabelType, false, $note);
} }
elseif ($newAmount < $productDetails->stock_amount + $containerWeight) elseif ($newAmount < $productDetails->stock_amount + $containerWeight)
{ {
@ -1629,6 +1638,7 @@ class StockService extends BaseService
{ {
$this->getDatabaseService()->ExecuteDbStatement('UPDATE stock SET stock_id = \'' . $splittedStockEntry->stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\''); $this->getDatabaseService()->ExecuteDbStatement('UPDATE stock SET stock_id = \'' . $splittedStockEntry->stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\'');
$this->getDatabaseService()->ExecuteDbStatement('UPDATE stock_log SET stock_id = \'' . $splittedStockEntry->stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\''); $this->getDatabaseService()->ExecuteDbStatement('UPDATE stock_log SET stock_id = \'' . $splittedStockEntry->stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\'');
$this->getDatabaseService()->ExecuteDbStatement('UPDATE userfield_values SET object_id = \'' . $splittedStockEntry->stock_id_to_keep . '\' WHERE field_id IN (SELECT id FROM userfields WHERE entity = \'stock\') AND object_id = \'' . $stockId . '\'');
} }
} }

View File

@ -132,6 +132,27 @@
</div> </div>
@endif @endif
<div class="form-group">
<label for="note">
{{ $__t('Note') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This will apply to added products') }}"></i>
</label>
<div class="input-group">
<input type="text"
class="form-control"
id="note"
name="note">
</div>
</div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'stock'
))
<button id="save-inventory-button" <button id="save-inventory-button"
class="btn btn-success">{{ $__t('OK') }}</button> class="btn btn-success">{{ $__t('OK') }}</button>

View File

@ -162,6 +162,21 @@
</div> </div>
@endif @endif
<div class="form-group">
<label for="note">{{ $__t('Note') }}</label>
<div class="input-group">
<input type="text"
class="form-control"
id="note"
name="note">
</div>
</div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'stock'
))
<button id="save-purchase-button" <button id="save-purchase-button"
class="btn btn-success d-block">{{ $__t('OK') }}</button> class="btn btn-success d-block">{{ $__t('OK') }}</button>

View File

@ -73,9 +73,14 @@
data-shadow-rowgroup-column="9">{{ $__t('Purchased date') }}</th> data-shadow-rowgroup-column="9">{{ $__t('Purchased date') }}</th>
<th class="d-none">Hidden purchased_date</th> <th class="d-none">Hidden purchased_date</th>
<th>{{ $__t('Timestamp') }}</th> <th>{{ $__t('Timestamp') }}</th>
<th>{{ $__t('Note') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', array(
'userfields' => $userfields 'userfields' => $userfieldsProducts
))
@include('components.userfields_thead', array(
'userfields' => $userfieldsStock
)) ))
</tr> </tr>
</thead> </thead>
@ -274,10 +279,18 @@
<time class="timeago timeago-contextual" <time class="timeago timeago-contextual"
datetime="{{ $stockEntry->row_created_timestamp }}"></time> datetime="{{ $stockEntry->row_created_timestamp }}"></time>
</td> </td>
<td>
<span id="stock-{{ $stockEntry->id }}-note">{{ $stockEntry->note }}</span>
</td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', array(
'userfields' => $userfields, 'userfields' => $userfieldsProducts,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $stockEntry->product_id) 'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValuesProducts, 'object_id', $stockEntry->product_id)
))
@include('components.userfields_tbody', array(
'userfields' => $userfieldsStock,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValuesStock, 'object_id', $stockEntry->stock_id)
)) ))
</tr> </tr>

View File

@ -129,6 +129,17 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="note">{{ $__t('Note') }}</label>
<div class="input-group">
<input type="text"
class="form-control"
id="note"
name="note"
value="{{ $stockEntry->note }}">
</div>
</div>
<button id="save-stockentry-button" <button id="save-stockentry-button"
class="btn btn-success">{{ $__t('OK') }}</button> class="btn btn-success">{{ $__t('OK') }}</button>

View File

@ -147,6 +147,11 @@
<th class="allow-grouping">{{ $__t('Transaction type') }}</th> <th class="allow-grouping">{{ $__t('Transaction type') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}</th> <th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif allow-grouping">{{ $__t('Location') }}</th>
<th class="allow-grouping">{{ $__t('Done by') }}</th> <th class="allow-grouping">{{ $__t('Done by') }}</th>
<th>{{ $__t('Note') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfieldsStock
))
</tr> </tr>
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@ -193,6 +198,14 @@
<td> <td>
{{ $stockLogEntry->user_display_name }} {{ $stockLogEntry->user_display_name }}
</td> </td>
<td>
{{ $stockLogEntry->note }}
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfieldsStock,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValuesStock, 'object_id', $stockLogEntry->stock_id)
))
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>