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) +
+ move_on_open == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="move_on_open" name="move_on_open" value="1"> + +
+ @endif + @else