diff --git a/grocy.openapi.json b/grocy.openapi.json index ee661c4c..4aca8008 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -981,7 +981,7 @@ "type": "object", "properties": { "amount": { - "type": "integer" + "type": "double" }, "best_before_date": { "type": "string", @@ -1056,7 +1056,7 @@ "type": "object", "properties": { "amount": { - "type": "integer", + "type": "double", "description": "The amount to remove" }, "transaction_type": { @@ -1191,7 +1191,7 @@ "type": "object", "properties": { "amount": { - "type": "integer", + "type": "double", "description": "The amount to mark as opened" }, "stock_entry_id": { @@ -1870,6 +1870,9 @@ "picture_file_name": { "type": "string" }, + "allow_partial_units_in_stock": { + "type": "boolean" + }, "row_created_timestamp": { "type": "string", "format": "date-time" @@ -1925,7 +1928,7 @@ "type": "integer" }, "amount": { - "type": "integer" + "type": "double" }, "best_before_date": { "type": "string", @@ -2176,7 +2179,7 @@ "type": "string" }, "amount": { - "type": "integer", + "type": "double", "minimum": 0, "default": 0, "description": "The manual entered amount" @@ -2289,7 +2292,7 @@ "type": "integer" }, "amount": { - "type": "integer" + "type": "double" }, "best_before_date": { "type": "string", @@ -2345,7 +2348,7 @@ "type": "integer" }, "amount": { - "type": "integer" + "type": "double" }, "best_before_date": { "type": "string", diff --git a/migrations/0049.sql b/migrations/0049.sql new file mode 100644 index 00000000..eb3a741f --- /dev/null +++ b/migrations/0049.sql @@ -0,0 +1,69 @@ +ALTER TABLE products +ADD allow_partial_units_in_stock TINYINT NOT NULL DEFAULT 0; + +PRAGMA legacy_alter_table = ON; + +ALTER TABLE stock RENAME TO stock_old; + +CREATE TABLE stock ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + product_id INTEGER NOT NULL, + amount DECIMAL(15, 2) NOT NULL, + best_before_date DATE, + purchased_date DATE DEFAULT (datetime('now', 'localtime')), + stock_id TEXT NOT NULL, + price DECIMAL(15, 2), + open TINYINT NOT NULL DEFAULT 0 CHECK(open IN (0, 1)), + opened_date DATETIME, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +INSERT INTO stock + (product_id, amount, best_before_date, purchased_date, stock_id, price, open, opened_date, row_created_timestamp) +SELECT product_id, amount, best_before_date, purchased_date, stock_id, price, open, opened_date, row_created_timestamp +FROM stock_old; + +DROP TABLE stock_old; + +ALTER TABLE stock_log RENAME TO stock_log_old; + +CREATE TABLE stock_log ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + product_id INTEGER NOT NULL, + amount DECIMAL(15, 2) NOT NULL, + best_before_date DATE, + purchased_date DATE, + used_date DATE, + spoiled INTEGER NOT NULL DEFAULT 0, + stock_id TEXT NOT NULL, + transaction_type TEXT NOT NULL, + price DECIMAL(15, 2), + undone TINYINT NOT NULL DEFAULT 0 CHECK(undone IN (0, 1)), + undone_timestamp DATETIME, + opened_date DATETIME, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +INSERT INTO stock_log + (product_id, amount, best_before_date, purchased_date, used_date, spoiled, stock_id, transaction_type, price, undone, undone_timestamp, opened_date, row_created_timestamp) +SELECT product_id, amount, best_before_date, purchased_date, used_date, spoiled, stock_id, transaction_type, price, undone, undone_timestamp, opened_date, row_created_timestamp +FROM stock_log_old; + +DROP TABLE stock_log_old; + +ALTER TABLE shopping_list RENAME TO shopping_list_old; + +CREATE TABLE shopping_list ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + product_id INTEGER, + note TEXT, + amount DECIMAL(15, 2) NOT NULL DEFAULT 0, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +INSERT INTO shopping_list + (product_id, amount, note, row_created_timestamp) +SELECT product_id, amount, note, row_created_timestamp +FROM shopping_list_old; + +DROP TABLE shopping_list_old; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index b0b4ae6d..ecfaee0a 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -133,6 +133,19 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) $('#amount').attr('max', productDetails.stock_amount); $('#amount_qu_unit').text(productDetails.quantity_unit_stock.name); + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + if ((productDetails.stock_amount || 0) === 0) { Grocy.Components.ProductPicker.SetValue(''); diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 983461fe..75c42c65 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -83,6 +83,19 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) $('#new_amount').attr('not-equal', productDetails.stock_amount); $('#new_amount_qu_unit').text(productDetails.quantity_unit_stock.name); + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#new_amount").attr("min", "0.01"); + $("#new_amount").attr("step", "0.01"); + $("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#new_amount").attr("min", "1"); + $("#new_amount").attr("step", "1"); + $("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + $('#new_amount').focus(); }, function(xhr) diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index ca93c725..dc2d45a3 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -9,7 +9,7 @@ redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val()); } - var jsonData = $('#product-form').serializeJSON(); + var jsonData = $('#product-form').serializeJSON({ checkboxUncheckedValue: "0" }); Grocy.FrontendHelpers.BeginUiBusy("product-form"); if ($("#product-picture")[0].files.length > 0) diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index a4988438..5c2f0e3e 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -100,6 +100,19 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); $('#price').val(productDetails.last_price); + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + if (productDetails.product.default_best_before_days.toString() !== '0') { if (productDetails.product.default_best_before_days == -1) diff --git a/public/viewjs/recipeposform.js b/public/viewjs/recipeposform.js index ff5fde78..da9ae5f4 100644 --- a/public/viewjs/recipeposform.js +++ b/public/viewjs/recipeposform.js @@ -52,6 +52,20 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { $("#qu_id").val(productDetails.quantity_unit_stock.id); } + + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + $('#amount').focus(); Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); }, diff --git a/public/viewjs/shoppinglistform.js b/public/viewjs/shoppinglistform.js index ec6ceeb3..9b18689f 100644 --- a/public/viewjs/shoppinglistform.js +++ b/public/viewjs/shoppinglistform.js @@ -47,6 +47,20 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) function (productDetails) { $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); + + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + $('#amount').focus(); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); }, diff --git a/services/StockService.php b/services/StockService.php index f1365f4d..c4aa5294 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -109,7 +109,7 @@ class StockService extends BaseService } } - public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price) + public function AddProduct(int $productId, float $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price) { if (!$this->ProductExists($productId)) { @@ -151,7 +151,7 @@ class StockService extends BaseService } } - public function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default') + public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default') { if (!$this->ProductExists($productId)) { @@ -258,7 +258,7 @@ class StockService extends BaseService return $this->Database->lastInsertId(); } - public function OpenProduct(int $productId, int $amount, $specificStockEntryId = 'default') + public function OpenProduct(int $productId, float $amount, $specificStockEntryId = 'default') { if (!$this->ProductExists($productId)) { diff --git a/views/productform.blade.php b/views/productform.blade.php index 57515134..78eda810 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -126,6 +126,14 @@ 'additionalHtmlElements' => '

' )) +
+
+ + allow_partial_units_in_stock == 1) checked @endif class="form-check-input" type="checkbox" id="allow_partial_units_in_stock" name="allow_partial_units_in_stock" value="1"> + +
+
+