diff --git a/.gitignore b/.gitignore
index 9946de1e..678e80bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/public/node_modules
/vendor
/.release
-embedded.txt
\ No newline at end of file
+embedded.txt
+.DS_Store
diff --git a/grocy.openapi.json b/grocy.openapi.json
index d9a61478..f5f5bf41 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -4481,6 +4481,15 @@
"userfields": {
"type": "object",
"description": "Key/value pairs of userfields"
+ },
+ "should_not_be_frozen": {
+ "type": "integer"
+ },
+ "default_consume_location_id": {
+ "type": "integer"
+ },
+ "move_on_open": {
+ "type": "integer"
}
},
"example": {
@@ -4502,7 +4511,10 @@
"tare_weight": "0.0",
"not_check_stock_fulfillment_for_recipes": "0",
"shopping_location_id": null,
- "userfields": null
+ "userfields": null,
+ "should_not_be_frozen": "1",
+ "default_consume_location_id": "5",
+ "move_on_open": "1"
}
},
"QuantityUnit": {
@@ -4785,6 +4797,9 @@
"has_childs": {
"type": "boolean",
"description": "True when the product is a parent product of others"
+ },
+ "default_location": {
+ "$ref": "#/components/schemas/Location"
}
},
"example": {
@@ -4850,7 +4865,8 @@
"row_created_timestamp": "2019-05-02 20:12:25"
},
"average_shelf_life_days": -1,
- "spoil_rate_percent": 0
+ "spoil_rate_percent": 0,
+ "default_consume_location": null
}
},
"ProductPriceHistory": {
diff --git a/localization/strings.pot b/localization/strings.pot
index 80120c36..a7c88736 100644
--- a/localization/strings.pot
+++ b/localization/strings.pot
@@ -2326,3 +2326,12 @@ msgstr ""
msgid "Stock entries at this location will be consumed first"
msgstr ""
+
+msgid "Move on open"
+msgstr ""
+
+msgid "When enabled, on marking this product as opened, the corresponding amount will be moved to the default consume location"
+msgstr ""
+
+msgid "Moved to %1$s"
+msgstr ""
diff --git a/migrations/0190.sql b/migrations/0190.sql
new file mode 100644
index 00000000..04336994
--- /dev/null
+++ b/migrations/0190.sql
@@ -0,0 +1,2 @@
+ALTER TABLE products
+ADD move_on_open TINYINT NOT NULL DEFAULT 0 CHECK(move_on_open IN (0, 1));
diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js
index 6bb6adbf..91bfd144 100644
--- a/public/viewjs/consume.js
+++ b/public/viewjs/consume.js
@@ -189,6 +189,11 @@ $('#save-mark-as-open-button').on('click', function(e)
Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(jsonForm.amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural, true), productDetails.product.name) + '
' + __t("Undo") + '');
+ if (productDetails.product.move_on_open == 1)
+ {
+ toastr.info('' + __t("Moved to %1$s", productDetails.default_consume_location.name) + " ");
+ }
+
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount))
{
$('#display_amount').val(productDetails.product.quick_consume_amount);
diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js
index 339195f5..59a6bca8 100644
--- a/public/viewjs/productform.js
+++ b/public/viewjs/productform.js
@@ -239,8 +239,30 @@ $('#product-form input').keyup(function(event)
$('#location_id').change(function(event)
{
Grocy.FrontendHelpers.ValidateForm('product-form');
+ UpdateMoveOnOpen();
});
+$('#default_consume_location_id').change(function(event)
+{
+ UpdateMoveOnOpen();
+});
+
+function UpdateMoveOnOpen()
+{
+ var defaultLocation = $("#location_id :selected").val();
+ var consumeLocationLocation = $("#default_consume_location_id :selected").val();
+
+ if (!consumeLocationLocation || defaultLocation === consumeLocationLocation)
+ {
+ document.getElementById("move_on_open").checked = false;
+ $("#move_on_open").attr("disabled", true);
+ }
+ else
+ {
+ $("#move_on_open").attr("disabled", false);
+ }
+}
+
$('#product-form input').keydown(function(event)
{
if (event.keyCode === 13) // Enter
@@ -556,6 +578,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
}
});
+UpdateMoveOnOpen();
Grocy.FrontendHelpers.ValidateForm("product-form");
Grocy.Components.ProductPicker.GetPicker().trigger("change");
diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js
index 47d7a84f..03a1dec5 100644
--- a/public/viewjs/stockentries.js
+++ b/public/viewjs/stockentries.js
@@ -126,10 +126,26 @@ $(document).on('click', '.product-open-button', function(e)
Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1, 'stock_entry_id': specificStockEntryId },
function(bookingResponse)
{
- button.addClass("disabled");
- Grocy.FrontendHelpers.EndUiBusy();
- toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + '');
- RefreshStockEntryRow(stockRowId);
+ Grocy.Api.Get('stock/products/' + productId,
+ function(result)
+ {
+ button.addClass("disabled");
+ Grocy.FrontendHelpers.EndUiBusy();
+ toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + '');
+
+ if (result.product.move_on_open == 1)
+ {
+ toastr.info('' + __t("Moved to %1$s", result.default_consume_location.name) + " ");
+ }
+
+ RefreshStockEntryRow(stockRowId);
+ },
+ function(xhr)
+ {
+ Grocy.FrontendHelpers.EndUiBusy();
+ console.error(xhr);
+ }
+ );
},
function(xhr)
{
diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js
index 1762c3ee..3cb5bcb4 100755
--- a/public/viewjs/stockoverview.js
+++ b/public/viewjs/stockoverview.js
@@ -208,6 +208,12 @@ $(document).on('click', '.product-open-button', function(e)
Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + productQuName, productName) + '
' + __t("Undo") + '');
+
+ if (result.product.move_on_open == 1)
+ {
+ toastr.info('' + __t("Moved to %1$s", result.default_consume_location.name) + " ");
+ }
+
RefreshStatistics();
RefreshProductRow(productId);
},
diff --git a/services/StockService.php b/services/StockService.php
index 6cf09284..9e75e49b 100644
--- a/services/StockService.php
+++ b/services/StockService.php
@@ -734,6 +734,12 @@ class StockService extends BaseService
}
$spoilRate = ($consumeCountSpoiled * 100.0) / $consumeCount;
+ $defaultConsumeLocation = null;
+ if (!empty($product->default_consume_location_id))
+ {
+ $defaultConsumeLocation = $this->getDatabase()->locations($product->default_consume_location_id);
+ }
+
return [
'product' => $product,
'product_barcodes' => $productBarcodes,
@@ -758,6 +764,7 @@ class StockService extends BaseService
'spoil_rate_percent' => $spoilRate,
'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount,
'has_childs' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
+ 'default_consume_location' => $defaultConsumeLocation
];
}
@@ -1057,6 +1064,15 @@ class StockService extends BaseService
$amount = 0;
}
+
+ if ($product->move_on_open == 1)
+ {
+ $locationIdTo = $product->default_consume_location_id;
+ if (!empty($locationIdTo) && $locationIdTo != $stockEntry->location_id)
+ {
+ $this->TransferProduct($stockEntry->product_id, $stockEntry->amount, $stockEntry->location_id, $locationIdTo, $stockEntry->stock_id, $transactionId);
+ }
+ }
}
return $transactionId;
diff --git a/views/productform.blade.php b/views/productform.blade.php
index 430332aa..dd3e9c85 100644
--- a/views/productform.blade.php
+++ b/views/productform.blade.php
@@ -146,6 +146,22 @@
$location->id == $product->default_consume_location_id) selected="selected" @endif value="{{ $location->id }}">{{ $location->name }}
@endforeach
+
+ @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
+