diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md
index 527b8b33..c9d83bf0 100644
--- a/changelog/55_UNRELEASED_2019-xx-xx.md
+++ b/changelog/55_UNRELEASED_2019-xx-xx.md
@@ -20,6 +20,12 @@
- On using "Consume all ingredients needed by this recipe" and when it has a product attached, one unit of that product (per serving in purchase quantity unit) will be added to stock (with the proper price based on the recipe ingredients)
- (Thanks @kriddles for the intial work on this)
+### New feature: Freeze/Thaw products
+- New product options "Default best before days after freezing/thawing" to set how the best before date should be changed on freezing/thawing
+- New location option "Is freezer" to indicate if the location is a freezer
+- => When moving a product from/to a freezer location, the best before date is changed accordingly
+- There is also a new sub feature flag `FEATURE_FLAG_STOCK_PRODUCT_FREEZING` to disable this if you don't need it (defaults to `true`)
+
### Stock improvements/fixes
- The productcard gets now also refreshed after a transaction was posted (purchase/consume/etc.) (thanks @kriddles)
- The product field calories (kcal) now also allows decimal numbers
diff --git a/config-dist.php b/config-dist.php
index 35f6d83f..208bb59a 100644
--- a/config-dist.php
+++ b/config-dist.php
@@ -136,6 +136,7 @@ Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true);
+Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true);
Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true);
Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true);
diff --git a/grocy.openapi.json b/grocy.openapi.json
index c49ce067..cdf729de 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -3467,8 +3467,11 @@
"location_id": {
"type": "integer"
},
- "name": {
+ "location_name": {
"type": "string"
+ },
+ "location_is_freezer": {
+ "type": "integer"
}
},
"example": {
diff --git a/localization/demo_data.pot b/localization/demo_data.pot
index 51cc69a5..13858364 100644
--- a/localization/demo_data.pot
+++ b/localization/demo_data.pot
@@ -330,3 +330,6 @@ msgstr ""
msgid "This is a note"
msgstr ""
+
+msgid "Freezer"
+msgstr ""
diff --git a/localization/strings.pot b/localization/strings.pot
index 96ed6add..b16d4a5c 100644
--- a/localization/strings.pot
+++ b/localization/strings.pot
@@ -1681,3 +1681,30 @@ msgstr ""
msgid "Scan mode is on but not all required fields could be populated automatically"
msgstr ""
+
+msgid "Is freezer"
+msgstr ""
+
+msgid "When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings"
+msgstr ""
+
+msgid "On moving this product to a freezer location, the best before date will be replaced by today + this amount of days"
+msgstr ""
+
+msgid "Default best before days after freezing"
+msgstr ""
+
+msgid "On moving this product from a freezer location, the best before date will be replaced by today + this amount of days"
+msgstr ""
+
+msgid "Default best before days after thawing"
+msgstr ""
+
+msgid "This cannot be the same as the \"From\" location"
+msgstr ""
+
+msgid "Thawed"
+msgstr ""
+
+msgid "Frozen"
+msgstr ""
diff --git a/migrations/0097.sql b/migrations/0097.sql
new file mode 100644
index 00000000..269911ef
--- /dev/null
+++ b/migrations/0097.sql
@@ -0,0 +1,31 @@
+ALTER TABLE products
+ADD default_best_before_days_after_freezing INTEGER NOT NULL DEFAULT 0;
+
+UPDATE products
+SET default_best_before_days_after_freezing = 0;
+
+ALTER TABLE products
+ADD default_best_before_days_after_thawing INTEGER NOT NULL DEFAULT 0;
+
+UPDATE products
+SET default_best_before_days_after_thawing = 0;
+
+ALTER TABLE locations
+ADD is_freezer TINYINT NOT NULL DEFAULT 0;
+
+UPDATE locations
+SET is_freezer = 0;
+
+DROP VIEW stock_current_locations;
+CREATE VIEW stock_current_locations
+AS
+SELECT
+ 1 AS id, -- Dummy, LessQL needs an id column
+ s.product_id,
+ s.location_id AS location_id,
+ l.name AS location_name,
+ l.is_freezer AS location_is_freezer
+FROM stock s
+JOIN locations l
+ ON s.location_id = l.id
+GROUP BY s.product_id, s.location_id, l.name;
diff --git a/public/viewjs/transfer.js b/public/viewjs/transfer.js
index 83540394..0824f9f3 100644
--- a/public/viewjs/transfer.js
+++ b/public/viewjs/transfer.js
@@ -1,4 +1,4 @@
-$('#save-transfer-button').on('click', function(e)
+$('#save-transfer-button').on('click', function (e)
{
e.preventDefault();
@@ -71,10 +71,18 @@
}
else
{
-
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
toastr.success(successMessage);
+ if (parseInt($("#location_id_from option:selected").attr("data-is-freezer")) === 0 && parseInt($("#location_id_to option:selected").attr("data-is-freezer")) === 1) // Frozen
+ {
+ toastr.info('' + __t("Frozen") + " ");
+ }
+ if (parseInt($("#location_id_from option:selected").attr("data-is-freezer")) === 1 && parseInt($("#location_id_to option:selected").attr("data-is-freezer")) === 0) // Thawed
+ {
+ toastr.info('' + __t("Thawed") + " ");
+ }
+
$("#specific_stock_entry").find("option").remove().end().append("");
$("#specific_stock_entry").attr("disabled", "");
$("#specific_stock_entry").removeAttr("required");
@@ -151,7 +159,8 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{
$("#location_id_from").append($("