From 0be672aa48a41f7dd193be450be5adc77fea7c5f Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 13:35:48 +0100 Subject: [PATCH 001/134] Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions (fixes #428) --- changelog/55_UNRELEASED_2019-xx-xx.md | 5 ++++- views/shoppinglist.blade.php | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 06d9f6e3..ca4d540f 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -4,7 +4,10 @@ - From there you can also edit the stock entries - A huge THANK YOU goes to @kriddles for the work on this feature -## Recipe improvements +### Shopping list fixes +- Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions + +### Recipe improvements - When adding or editing a recipe ingredient, a dialog is now used instead of switching between pages (thanks @kriddles) ### Meal plan fixes diff --git a/views/shoppinglist.blade.php b/views/shoppinglist.blade.php index 706ed3af..08d3cfb3 100644 --- a/views/shoppinglist.blade.php +++ b/views/shoppinglist.blade.php @@ -45,6 +45,8 @@ --> +@else + @endif
From 675bf25927e282a38005cf2e27728fa0ffe99c2c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 13:50:10 +0100 Subject: [PATCH 002/134] Allow empty date(time) inputs when the field is not required (fixes #462( --- changelog/55_UNRELEASED_2019-xx-xx.md | 3 +++ public/viewjs/components/datetimepicker.js | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index ca4d540f..a52c6ca0 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -16,6 +16,9 @@ ### Calendar improvements - Improved that meal plan events in the iCal calendar export now contain a link to the appropriate meal plan week in the body of the event (thanks @kriddles) +### Task fixes +- Fixed that a due date was required when editing an existing task + ### API improvements/fixes - Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002) - New endpoints for the stock transfer & stock entry edit capabilities mentioned above diff --git a/public/viewjs/components/datetimepicker.js b/public/viewjs/components/datetimepicker.js index d673a942..a785b072 100644 --- a/public/viewjs/components/datetimepicker.js +++ b/public/viewjs/components/datetimepicker.js @@ -221,7 +221,10 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) var element = Grocy.Components.DateTimePicker.GetInputElement()[0]; if (!dateObj.isValid()) { - element.setCustomValidity("error"); + if ($(element).hasAttr("required")) + { + element.setCustomValidity("error"); + } } else { From 8e26bd2c310681d3a8b763c3d9ae285cfe68f95a Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 13:55:14 +0100 Subject: [PATCH 003/134] Allow partial units during inventory (fixes #459) --- changelog/55_UNRELEASED_2019-xx-xx.md | 3 +++ services/StockService.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index a52c6ca0..7ba8f5c6 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -4,6 +4,9 @@ - From there you can also edit the stock entries - A huge THANK YOU goes to @kriddles for the work on this feature +### Stock fixes +- Fixed that entering partial amounts was not possible on the inventory page (only applies if the product option "Allow partial units in stock" is enabled) + ### Shopping list fixes - Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions diff --git a/services/StockService.php b/services/StockService.php index 2e42dd20..efde1419 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -604,7 +604,7 @@ class StockService extends BaseService return $returnValue; } - public function InventoryProduct(int $productId, int $newAmount, $bestBeforeDate, $locationId = null, $price = null) + public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null) { if (!$this->ProductExists($productId)) { From 6345e69922751b68de66e1075539b46ed694cd92 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 14:10:43 +0100 Subject: [PATCH 004/134] Fixed tare weight handling min. amount on purchase was not calculated based on the products qu_factor_purchase_to_stock (fixes #457) --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + public/viewjs/purchase.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 7ba8f5c6..9027446c 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -6,6 +6,7 @@ ### Stock fixes - Fixed that entering partial amounts was not possible on the inventory page (only applies if the product option "Allow partial units in stock" is enabled) +- Fixed that on purchase a wrong minimum amount was enforced for products with enabled tare weight handling in combination with different purchase/stock quantity units ### Shopping list fixes - Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 26073c20..368264cc 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -160,8 +160,9 @@ if (Grocy.Components.ProductPicker !== undefined) if (productDetails.product.enable_tare_weight_handling == 1) { - var minAmount = parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount) + 1; + var minAmount = parseFloat(productDetails.product.tare_weight) / productDetails.product.qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount); $("#amount").attr("min", minAmount); + $("#amount").attr("step", "0.0001"); $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString())); $("#tare-weight-handling-info").removeClass("d-none"); } From e515f21d3b3f6b0705367a73915e15c4555a7535 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 14:18:56 +0100 Subject: [PATCH 005/134] Fixed DataTables earch / don't search the first column with buttons/menus (fixes #440) --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + public/viewjs/batteries.js | 3 ++- public/viewjs/batteriesjournal.js | 3 ++- public/viewjs/batteriesoverview.js | 3 ++- public/viewjs/chores.js | 3 ++- public/viewjs/choresjournal.js | 3 ++- public/viewjs/choresoverview.js | 3 ++- public/viewjs/locations.js | 3 ++- public/viewjs/manageapikeys.js | 3 ++- public/viewjs/productform.js | 1 + public/viewjs/productgroups.js | 3 ++- public/viewjs/products.js | 3 ++- public/viewjs/quantityunitform.js | 3 ++- public/viewjs/quantityunits.js | 3 ++- public/viewjs/recipeform.js | 4 +++- public/viewjs/shoppinglist.js | 1 + public/viewjs/stockdetail.js | 1 + public/viewjs/stockjournal.js | 3 ++- public/viewjs/stockoverview.js | 1 + public/viewjs/taskcategories.js | 3 ++- public/viewjs/tasks.js | 1 + public/viewjs/userentities.js | 3 ++- public/viewjs/userfields.js | 3 ++- public/viewjs/userobjects.js | 3 ++- public/viewjs/users.js | 3 ++- 25 files changed, 45 insertions(+), 19 deletions(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 9027446c..a4be6368 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -28,6 +28,7 @@ - New endpoints for the stock transfer & stock entry edit capabilities mentioned above ### General & other improvements/fixes +- Fixed that also the first column (where in most tables only buttons/menus are displayed) in tables was searched when using the general search field - Fixed that the meal plan menu entry (sidebar) was not visible when the calendar was disabled (`FEATURE_FLAG_CALENDAR`) (thanks @lwis) - Slightly optimized table loading & search performance (thanks @lwis) - For integration: If a `GET` parameter `closeAfterCreation` is passed to the product edit page, the window will be closed on save (due to Browser restrictions, this only works when the window was opened from JavaScript) (thanks @Forceu) diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js index d08d143b..893e8ed9 100644 --- a/public/viewjs/batteries.js +++ b/public/viewjs/batteries.js @@ -1,7 +1,8 @@ var batteriesTable = $('#batteries-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ], }); $('#batteries-table tbody').removeClass("d-none"); diff --git a/public/viewjs/batteriesjournal.js b/public/viewjs/batteriesjournal.js index 0466b4d1..6b20ff02 100644 --- a/public/viewjs/batteriesjournal.js +++ b/public/viewjs/batteriesjournal.js @@ -2,7 +2,8 @@ 'paginate': true, 'order': [[1, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#batteries-journal-table tbody').removeClass("d-none"); diff --git a/public/viewjs/batteriesoverview.js b/public/viewjs/batteriesoverview.js index ef462f67..f08e8a92 100644 --- a/public/viewjs/batteriesoverview.js +++ b/public/viewjs/batteriesoverview.js @@ -1,7 +1,8 @@ var batteriesOverviewTable = $('#batteries-overview-table').DataTable({ 'order': [[2, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#batteries-overview-table tbody').removeClass("d-none"); diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js index 848c9259..8c600cf6 100644 --- a/public/viewjs/chores.js +++ b/public/viewjs/chores.js @@ -1,7 +1,8 @@ var choresTable = $('#chores-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#chores-table tbody').removeClass("d-none"); diff --git a/public/viewjs/choresjournal.js b/public/viewjs/choresjournal.js index c80e495f..7ca81ea8 100644 --- a/public/viewjs/choresjournal.js +++ b/public/viewjs/choresjournal.js @@ -2,7 +2,8 @@ 'paginate': true, 'order': [[1, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#chores-journal-table tbody').removeClass("d-none"); diff --git a/public/viewjs/choresoverview.js b/public/viewjs/choresoverview.js index 33428010..65019c93 100644 --- a/public/viewjs/choresoverview.js +++ b/public/viewjs/choresoverview.js @@ -1,7 +1,8 @@ var choresOverviewTable = $('#chores-overview-table').DataTable({ 'order': [[2, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#chores-overview-table tbody').removeClass("d-none"); diff --git a/public/viewjs/locations.js b/public/viewjs/locations.js index ec4a089f..09b0d3ed 100644 --- a/public/viewjs/locations.js +++ b/public/viewjs/locations.js @@ -1,7 +1,8 @@ var locationsTable = $('#locations-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#locations-table tbody').removeClass("d-none"); diff --git a/public/viewjs/manageapikeys.js b/public/viewjs/manageapikeys.js index 79de8144..5017b2cc 100644 --- a/public/viewjs/manageapikeys.js +++ b/public/viewjs/manageapikeys.js @@ -1,7 +1,8 @@ var apiKeysTable = $('#apikeys-table').DataTable({ 'order': [[4, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#apikeys-table tbody').removeClass("d-none"); diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index 4be401d7..1990cef4 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -354,6 +354,7 @@ var quConversionsTable = $('#qu-conversions-table').DataTable({ "orderFixed": [[3, 'asc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 }, { 'visible': false, 'targets': 3 } ], 'rowGroup': { diff --git a/public/viewjs/productgroups.js b/public/viewjs/productgroups.js index 618b2671..d935977b 100644 --- a/public/viewjs/productgroups.js +++ b/public/viewjs/productgroups.js @@ -1,7 +1,8 @@ var groupsTable = $('#productgroups-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#productgroups-table tbody').removeClass("d-none"); diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 97c3b8c6..2a040a7d 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -1,7 +1,8 @@ var productsTable = $('#products-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#products-table tbody').removeClass("d-none"); diff --git a/public/viewjs/quantityunitform.js b/public/viewjs/quantityunitform.js index f6044b40..a2017b1c 100644 --- a/public/viewjs/quantityunitform.js +++ b/public/viewjs/quantityunitform.js @@ -116,7 +116,8 @@ $('#quantityunit-form input').keydown(function(event) var quConversionsTable = $('#qu-conversions-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#qu-conversions-table tbody').removeClass("d-none"); diff --git a/public/viewjs/quantityunits.js b/public/viewjs/quantityunits.js index b4ad2a45..bb7ba589 100644 --- a/public/viewjs/quantityunits.js +++ b/public/viewjs/quantityunits.js @@ -1,7 +1,8 @@ var quantityUnitsTable = $('#quantityunits-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#quantityunits-table tbody').removeClass("d-none"); diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js index c146fb11..78cfdb40 100644 --- a/public/viewjs/recipeform.js +++ b/public/viewjs/recipeform.js @@ -66,6 +66,7 @@ var recipesPosTables = $('#recipes-pos-table').DataTable({ "orderFixed": [[4, 'asc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 }, { 'visible': false, 'targets': 4 } ], 'rowGroup': { @@ -78,7 +79,8 @@ recipesPosTables.columns.adjust().draw(); var recipesIncludesTables = $('#recipes-includes-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#recipes-includes-table tbody').removeClass("d-none"); diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index e7b93afa..2991b6a6 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -3,6 +3,7 @@ "orderFixed": [[3, 'asc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 }, { 'visible': false, 'targets': 3 } ], 'rowGroup': { diff --git a/public/viewjs/stockdetail.js b/public/viewjs/stockdetail.js index ab871201..3cba3c47 100644 --- a/public/viewjs/stockdetail.js +++ b/public/viewjs/stockdetail.js @@ -2,6 +2,7 @@ 'order': [[2, 'asc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ], }); $('#stock-detail-table tbody').removeClass("d-none"); diff --git a/public/viewjs/stockjournal.js b/public/viewjs/stockjournal.js index 4e5f6ae0..af3ad490 100644 --- a/public/viewjs/stockjournal.js +++ b/public/viewjs/stockjournal.js @@ -2,7 +2,8 @@ 'paginate': true, 'order': [[3, 'desc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#stock-journal-table tbody').removeClass("d-none"); diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 645e2ac9..4047f197 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -2,6 +2,7 @@ 'order': [[3, 'asc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 }, { 'visible': false, 'targets': 4 }, { 'visible': false, 'targets': 5 }, { 'visible': false, 'targets': 6 } diff --git a/public/viewjs/taskcategories.js b/public/viewjs/taskcategories.js index 396ba8f0..16732e64 100644 --- a/public/viewjs/taskcategories.js +++ b/public/viewjs/taskcategories.js @@ -1,7 +1,8 @@ var categoriesTable = $('#taskcategories-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#taskcategories-table tbody').removeClass("d-none"); diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js index 3dbd6d30..f270445f 100644 --- a/public/viewjs/tasks.js +++ b/public/viewjs/tasks.js @@ -2,6 +2,7 @@ 'order': [[2, 'desc']], 'columnDefs': [ { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 }, { 'visible': false, 'targets': 3 } ], 'rowGroup': { diff --git a/public/viewjs/userentities.js b/public/viewjs/userentities.js index a9b3a24e..c26dc1b9 100644 --- a/public/viewjs/userentities.js +++ b/public/viewjs/userentities.js @@ -1,7 +1,8 @@ var userentitiesTable = $('#userentities-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#userentities-table tbody').removeClass("d-none"); diff --git a/public/viewjs/userfields.js b/public/viewjs/userfields.js index f9823a00..648126d7 100644 --- a/public/viewjs/userfields.js +++ b/public/viewjs/userfields.js @@ -1,7 +1,8 @@ var userfieldsTable = $('#userfields-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#userfields-table tbody').removeClass("d-none"); diff --git a/public/viewjs/userobjects.js b/public/viewjs/userobjects.js index 0f96bed1..5b348e1a 100644 --- a/public/viewjs/userobjects.js +++ b/public/viewjs/userobjects.js @@ -1,7 +1,8 @@ var userobjectsTable = $('#userobjects-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#userobjects-table tbody').removeClass("d-none"); diff --git a/public/viewjs/users.js b/public/viewjs/users.js index 64e9019c..b3961be5 100644 --- a/public/viewjs/users.js +++ b/public/viewjs/users.js @@ -1,7 +1,8 @@ var usersTable = $('#users-table').DataTable({ 'order': [[1, 'asc']], 'columnDefs': [ - { 'orderable': false, 'targets': 0 } + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } ] }); $('#users-table tbody').removeClass("d-none"); From 539334f5eec0c2080c45b1db555a3846b5175e29 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 3 Jan 2020 15:03:03 +0100 Subject: [PATCH 006/134] Fixed the response type description of the `/stock/volatile` API endpoint (fixes #460) --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + grocy.openapi.json | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index a4be6368..0ca3215f 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -25,6 +25,7 @@ ### API improvements/fixes - Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002) +- Fixed the response type description of the `/stock/volatile` endpoint - New endpoints for the stock transfer & stock entry edit capabilities mentioned above ### General & other improvements/fixes diff --git a/grocy.openapi.json b/grocy.openapi.json index 4fd92a4d..2c981663 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -4080,7 +4080,20 @@ "missing_products": { "type": "array", "items": { - "$ref": "#/components/schemas/CurrentStockResponse" + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "amount_missing": { + "type": "number" + }, + "is_partly_in_stock": { + "type": "integer" + } + } } } } From a8cf5ae9ab113cac7e379ca064f3b75068221f18 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 5 Jan 2020 09:11:11 +0100 Subject: [PATCH 007/134] Handle demo mode via a setting instead of checking the existence of a file (closes #484) --- README.md | 2 +- app.php | 20 ++++++-------------- changelog/55_UNRELEASED_2019-xx-xx.md | 3 ++- config-dist.php | 3 ++- controllers/SystemController.php | 2 +- middleware/ApiKeyAuthMiddleware.php | 2 +- middleware/SessionAuthMiddleware.php | 2 +- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b9339c74..c01e7414 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ If you don't use certain feature sets of grocy (for example if you don't need "C - When the file `data/custom_css.html` exists, the contents of the file will be added just before `` (end of head) on every page ### Demo mode -When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration. +When the `MODE` setting is set to `dev`, `demo` or `prerelease`, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration. ### Embedded mode When the file `embedded.txt` exists, it must contain a valid and writable path which will be used as the data directory instead of `data` and authentication will be disabled (used in [grocy-desktop](https://github.com/grocy/grocy-desktop)). diff --git a/app.php b/app.php index c4c54df7..491b1a54 100644 --- a/app.php +++ b/app.php @@ -19,20 +19,6 @@ else define('GROCY_DATAPATH', __DIR__ . '/data'); } -// Definitions for demo mode -if (file_exists(GROCY_DATAPATH . '/demo.txt')) -{ - define('GROCY_IS_DEMO_INSTALL', true); - if (!defined('GROCY_USER_ID')) - { - define('GROCY_USER_ID', 1); - } -} -else -{ - define('GROCY_IS_DEMO_INSTALL', false); -} - // Load composer dependencies require_once __DIR__ . '/vendor/autoload.php'; @@ -40,6 +26,12 @@ require_once __DIR__ . '/vendor/autoload.php'; require_once GROCY_DATAPATH . '/config.php'; require_once __DIR__ . '/config-dist.php'; // For not in own config defined values we use the default ones +// Definitions for dev/demo/prerelease mode +if (GROCY_MODE === 'dev' || GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease') +{ + define('GROCY_USER_ID', 1); +} + // Definitions for disabled authentication mode if (GROCY_DISABLE_AUTH === true) { diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 0ca3215f..f851ed91 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -33,6 +33,7 @@ - Fixed that the meal plan menu entry (sidebar) was not visible when the calendar was disabled (`FEATURE_FLAG_CALENDAR`) (thanks @lwis) - Slightly optimized table loading & search performance (thanks @lwis) - For integration: If a `GET` parameter `closeAfterCreation` is passed to the product edit page, the window will be closed on save (due to Browser restrictions, this only works when the window was opened from JavaScript) (thanks @Forceu) -- The `update.sh` file had wrong line endings (DOS instead of Unix) +- Fixed that the `update.sh` file had wrong line endings (DOS instead of Unix) +- Internal change: Demo mode is now handled via the setting `MODE` instead of checking the existence of the file `data/demo.txt` - New translations: (thanks all the translators) - Portuguese (Brazil) (demo available at https://pt-br.demo.grocy.info) diff --git a/config-dist.php b/config-dist.php index 9df72f0b..95dfbc32 100644 --- a/config-dist.php +++ b/config-dist.php @@ -15,7 +15,8 @@ # Either "production", "dev", "demo" or "prerelease" -# ("demo" and "prerelease" is reserved to be used only on the offical demo instances) +# When not "production", authentication will be disabled and +# demo data will be populated during database migrations Setting('MODE', 'production'); # Either "en" or "de" or the directory name of diff --git a/controllers/SystemController.php b/controllers/SystemController.php index 54cdb252..b212c001 100644 --- a/controllers/SystemController.php +++ b/controllers/SystemController.php @@ -22,7 +22,7 @@ class SystemController extends BaseController $databaseMigrationService = new DatabaseMigrationService(); $databaseMigrationService->MigrateDatabase(); - if (GROCY_IS_DEMO_INSTALL) + if (GROCY_MODE === 'dev' || GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease') { $demoDataGeneratorService = new DemoDataGeneratorService(); $demoDataGeneratorService->PopulateDemoData(); diff --git a/middleware/ApiKeyAuthMiddleware.php b/middleware/ApiKeyAuthMiddleware.php index f8a4de08..1dd238ed 100644 --- a/middleware/ApiKeyAuthMiddleware.php +++ b/middleware/ApiKeyAuthMiddleware.php @@ -22,7 +22,7 @@ class ApiKeyAuthMiddleware extends BaseMiddleware $route = $request->getAttribute('route'); $routeName = $route->getName(); - if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) + if (GROCY_MODE === 'dev' || GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease' || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { define('GROCY_AUTHENTICATED', true); $response = $next($request, $response); diff --git a/middleware/SessionAuthMiddleware.php b/middleware/SessionAuthMiddleware.php index 5cf436cc..adad241f 100644 --- a/middleware/SessionAuthMiddleware.php +++ b/middleware/SessionAuthMiddleware.php @@ -25,7 +25,7 @@ class SessionAuthMiddleware extends BaseMiddleware { $response = $next($request, $response); } - elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) + elseif (GROCY_MODE === 'dev' || GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease' || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { $user = $sessionService->GetDefaultUser(); define('GROCY_AUTHENTICATED', true); From 485eb262f99399bb61dd532a57943dbd335654bd Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 5 Jan 2020 09:20:58 +0100 Subject: [PATCH 008/134] Show some more info when camera access is not possible (closes #437) --- localization/strings.pot | 3 +++ public/viewjs/components/barcodescanner.js | 1 + 2 files changed, 4 insertions(+) diff --git a/localization/strings.pot b/localization/strings.pot index f77077e4..211374d2 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1606,3 +1606,6 @@ msgstr "" msgid "Edit stock entry" msgstr "" + +msgid "Camera access is on only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection" +msgstr "" diff --git a/public/viewjs/components/barcodescanner.js b/public/viewjs/components/barcodescanner.js index 82d3973e..b43b2f83 100644 --- a/public/viewjs/components/barcodescanner.js +++ b/public/viewjs/components/barcodescanner.js @@ -63,6 +63,7 @@ Grocy.Components.BarcodeScanner.StartScanning = function() if (error) { Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message); + toastr.info(__t("Camera access is on only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection")); setTimeout(function() { bootbox.hideAll(); From d4bec3bd10eebe4585cf33cf4badd9617eb48a7e Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 5 Jan 2020 10:03:02 +0100 Subject: [PATCH 009/134] Added a "keep screen on" option using NoSleep.js (closes #427) --- changelog/55_UNRELEASED_2019-xx-xx.md | 2 + config-dist.php | 4 ++ localization/strings.pot | 6 +++ package.json | 1 + public/js/grocy_wakelockhandling.js | 72 +++++++++++++++++++++++++++ views/layout/default.blade.php | 19 +++++++ yarn.lock | 5 ++ 7 files changed, 109 insertions(+) create mode 100644 public/js/grocy_wakelockhandling.js diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index f851ed91..57b43652 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -29,6 +29,8 @@ - New endpoints for the stock transfer & stock entry edit capabilities mentioned above ### General & other improvements/fixes +- It's now possible to keep the screen on always or when a "fullscreen-card" (e. g. used for recipes) is displayed + - New user options in the display settings menu in the top right corner (default is disabled) - Fixed that also the first column (where in most tables only buttons/menus are displayed) in tables was searched when using the general search field - Fixed that the meal plan menu entry (sidebar) was not visible when the calendar was disabled (`FEATURE_FLAG_CALENDAR`) (thanks @lwis) - Slightly optimized table loading & search performance (thanks @lwis) diff --git a/config-dist.php b/config-dist.php index 95dfbc32..bf2ca90a 100644 --- a/config-dist.php +++ b/config-dist.php @@ -75,6 +75,10 @@ DefaultUserSetting('auto_night_mode_time_range_to', "07:00"); // Format HH:mm DefaultUserSetting('auto_night_mode_time_range_goes_over_midnight', true); // If the time range above goes over midnight DefaultUserSetting('currently_inside_night_mode_range', false); // If we're currently inside of night mode time range (this is not user configurable, but stored as a user setting because it's evaluated client side to be able to use the client time instead of the maybe different server time) +# Keep screen on settings +DefaultUserSetting('keep_screen_on', false); // Keep the screen always on +DefaultUserSetting('keep_screen_on_when_fullscreen_card', false); // Keep the screen on when a "fullscreen-card" is displayed + # Stock settings DefaultUserSetting('product_presets_location_id', -1); // Default location id for new products (-1 means no location is preset) DefaultUserSetting('product_presets_product_group_id', -1); // Default product group id for new products (-1 means no product group is preset) diff --git a/localization/strings.pot b/localization/strings.pot index 211374d2..408f8b77 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1609,3 +1609,9 @@ msgstr "" msgid "Camera access is on only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection" msgstr "" + +msgid "Keep screen on" +msgstr "" + +msgid "Keep screen on while displaying a \"fullscreen-card\"" +msgstr "" diff --git a/package.json b/package.json index 3304ae6c..eaadff35 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "jquery-serializejson": "^2.9.0", "jquery-ui-dist": "^1.12.1", "moment": "^2.24.0", + "nosleep.js": "^0.9.0", "quagga": "^0.12.1", "sprintf-js": "^1.1.2", "startbootstrap-sb-admin": "^4.0.0", diff --git a/public/js/grocy_wakelockhandling.js b/public/js/grocy_wakelockhandling.js new file mode 100644 index 00000000..a18b3a31 --- /dev/null +++ b/public/js/grocy_wakelockhandling.js @@ -0,0 +1,72 @@ +Grocy.WakeLock = { }; +Grocy.WakeLock.NoSleepJsIntance = null; +Grocy.WakeLock.InitDone = false; + +$("#keep_screen_on").on("change", function() +{ + var value = $(this).is(":checked"); + if (value) + { + Grocy.WakeLock.Enable(); + } + else + { + Grocy.WakeLock.Disable(); + } +}); + +Grocy.WakeLock.Enable = function() +{ + if (Grocy.WakeLock.NoSleepJsIntance === null) + { + Grocy.WakeLock.NoSleepJsIntance = new NoSleep(); + } + Grocy.WakeLock.NoSleepJsIntance.enable(); + Grocy.WakeLock.InitDone = true; +} + +Grocy.WakeLock.Disable = function() +{ + if (Grocy.WakeLock.NoSleepJsIntance !== null) + { + Grocy.WakeLock.NoSleepJsIntance.disable(); + } +} + +// Handle "Keep screen on while displaying a fullscreen-card" when the body class "fullscreen-card" has changed +new MutationObserver(function(mutations) +{ + if (BoolVal(Grocy.UserSettings.keep_screen_on_when_fullscreen_card) && !BoolVal(Grocy.UserSettings.keep_screen_on)) + { + mutations.forEach(function(mutation) + { + if (mutation.attributeName === "class") + { + var attributeValue = $(mutation.target).prop(mutation.attributeName); + if (attributeValue.contains("fullscreen-card")) + { + Grocy.WakeLock.Enable(); + } + else + { + Grocy.WakeLock.Disable(); + } + } + }); + } +}).observe(document.body, { + attributes: true +}); + +// Enabling NoSleep.Js only works in a user input event handler, +// so if the user wants to keep the screen on always, +// do this in on the first click on anything +$(document).click(function() +{ + if (Grocy.WakeLock.InitDone === false && BoolVal(Grocy.UserSettings.keep_screen_on)) + { + Grocy.WakeLock.Enable(); + } + + Grocy.WakeLock.InitDone = true; +}); diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 47816c64..3d70c80c 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -360,6 +360,23 @@
+ + + @endif @@ -430,10 +447,12 @@ @if(!empty($__t('bootstrap-select_locale') && $__t('bootstrap-select_locale') != 'x'))@endif + + @stack('pageScripts') diff --git a/yarn.lock b/yarn.lock index 3d5835da..1e6d612a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,6 +557,11 @@ node-bitmap@0.0.1: resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091" integrity sha1-GA6scAPgxwdhjvMTaPYvhLKmkJE= +nosleep.js@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/nosleep.js/-/nosleep.js-0.9.0.tgz#0f1371b81dc182e3b6bbdb837e880f16db9d7163" + integrity sha512-qLOl2MmuGOPZY7Exi0kYJSCr2e9IcAtOykOo7hXUGAoaMC1Iqj0m+Aj2REuay68mDkhbc5CoA4ccUvcZI175Kw== + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" From 2a608c41e9f928a866eaff2a6478652d140f8a6d Mon Sep 17 00:00:00 2001 From: kriddles <54413450+kriddles@users.noreply.github.com> Date: Fri, 17 Jan 2020 10:54:34 -0600 Subject: [PATCH 010/134] Stock detail updates (#493) * Fix spelling * stockdetail refresh with location name * Stock updates * change stock_row_id to id * fix stockdetail refresh rows after clicking undo * fix stockdetail consume spoiled --- controllers/StockApiController.php | 11 +++++-- grocy.openapi.json | 48 +++++++++++++++++++++++++++--- public/viewjs/stockdetail.js | 36 ++++++++++++++++++---- public/viewjs/stockedit.js | 20 ++----------- routes.php | 3 +- services/StockService.php | 5 ++++ views/stockdetail.blade.php | 5 +++- 7 files changed, 96 insertions(+), 32 deletions(-) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index c9cc5538..133e5cd9 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -124,7 +124,7 @@ class StockApiController extends BaseApiController throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); } - if (!array_key_exists('stock_row_id', $requestBody)) + if (!array_key_exists('id', $requestBody)) { throw new \Exception('A stock row id is required'); } @@ -152,7 +152,7 @@ class StockApiController extends BaseApiController $locationId = $requestBody['location_id']; } - $bookingId = $this->StockService->EditStock($requestBody['stock_row_id'], $requestBody['amount'], $bestBeforeDate, $locationId, $price); + $bookingId = $this->StockService->EditStock($requestBody['id'], $requestBody['amount'], $bestBeforeDate, $locationId, $price); return $this->ApiResponse($this->Database->stock_log($bookingId)); } catch (\Exception $ex) @@ -388,7 +388,7 @@ class StockApiController extends BaseApiController return $this->ApiResponse($this->StockService->GetCurrentStock()); } - public function CurrentVolatilStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + public function CurrentVolatileStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $nextXDays = 5; if (isset($request->getQueryParams()['expiring_days']) && !empty($request->getQueryParams()['expiring_days']) && is_numeric($request->getQueryParams()['expiring_days'])) @@ -580,6 +580,11 @@ class StockApiController extends BaseApiController return $this->ApiResponse($this->StockService->GetProductStockLocations($args['productId'])); } + public function StockEntry(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->ApiResponse($this->StockService->GetStockEntry($args['entryId'])); + } + public function StockBooking(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { try diff --git a/grocy.openapi.json b/grocy.openapi.json index 2c981663..82480d78 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1063,10 +1063,10 @@ "schema": { "type": "object", "properties": { - "stock_row_id": { + "id": { "type": "number", "format": "number", - "description": "The Stock Row Id" + "description": "The stock table id" }, "amount": { "type": "number", @@ -1090,7 +1090,7 @@ } }, "example": { - "stock_row_id": 2, + "id": 2, "amount": 1, "best_before_date": "2019-01-19", "location_id": 2, @@ -1124,6 +1124,47 @@ } } }, + "/stock/{entryId}/entry": { + "get": { + "summary": "Returns details of the given stock", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "entryId", + "required": true, + "description": "A valid stock row id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A StockEntry Response object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/stock/volatile": { "get": { "summary": "Returns all products which are expiring soon, are already expired or currently missing", @@ -3203,7 +3244,6 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", - "stock", "recipes", "recipes_pos", "recipes_nestings", diff --git a/public/viewjs/stockdetail.js b/public/viewjs/stockdetail.js index 3cba3c47..2ee665c2 100644 --- a/public/viewjs/stockdetail.js +++ b/public/viewjs/stockdetail.js @@ -68,7 +68,7 @@ $(document).on('click', '.stock-consume-button', function(e) var stockRowId = $(e.currentTarget).attr('data-stockrow-id'); var consumeAmount = $(e.currentTarget).attr('data-consume-amount'); - var wasSpoiled = $(e.currentTarget).hasClass("product-consume-button-spoiled"); + var wasSpoiled = $(e.currentTarget).hasClass("stock-consume-button-spoiled"); Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled, 'location_id': locationId, 'stock_entry_id': specificStockEntryId}, function(bookingResponse) @@ -76,15 +76,15 @@ $(document).on('click', '.stock-consume-button', function(e) Grocy.Api.Get('stock/products/' + productId, function(result) { - var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; + var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; if (wasSpoiled) { toastMessage += " (" + __t("Spoiled") + ")"; } Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(toastMessage); RefreshStockDetailRow(stockRowId); + toastr.success(toastMessage); }, function(xhr) { @@ -215,7 +215,7 @@ $(document).on("click", ".product-add-to-shopping-list-button", function(e) function RefreshStockDetailRow(stockRowId) { - Grocy.Api.Get("objects/stock/" + stockRowId, + Grocy.Api.Get("stock/" + stockRowId + "/entry", function(result) { var stockRow = $('#stock-' + stockRowId + '-row'); @@ -249,11 +249,20 @@ function RefreshStockDetailRow(stockRowId) $(this).text(result.best_before_date).fadeIn(500); }); + var locationName = ""; + Grocy.Api.Get("objects/locations/" + result.location_id, + function(locationResult) + { + locationName = locationResult.name; + }, + function(xhr) + { + console.error(xhr); + }); $('#stock-' + stockRowId + '-location').parent().effect('highlight', { }, 500); $('#stock-' + stockRowId + '-location').fadeOut(500, function() { - //TODO grab location name instead of id - $(this).text(result.location_id).fadeIn(500); + $(this).text(locationName).fadeIn(500); }); $('#stock-' + stockRowId + '-price').parent().effect('highlight', { }, 500); @@ -292,3 +301,18 @@ $(window).on("message", function(e) RefreshStockDetailRow(data.Payload); } }); + +function UndoStockBookingEntry(bookingId, stockRowId) +{ + Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { }, + function(result) + { + window.postMessage(WindowMessageBag("StockDetailChanged", stockRowId), Grocy.BaseUrl); + toastr.success(__t("Booking successfully undone")); + }, + function(xhr) + { + console.error(xhr); + } + ); +}; diff --git a/public/viewjs/stockedit.js b/public/viewjs/stockedit.js index 24bea2b8..ca02abb2 100644 --- a/public/viewjs/stockedit.js +++ b/public/viewjs/stockedit.js @@ -1,6 +1,6 @@ $(document).ready(function() { var stockRowId = GetUriParam('stockRowId'); - Grocy.Api.Get("objects/stock/" + stockRowId, + Grocy.Api.Get("stock/" + stockRowId + "/entry", function(stockEntry) { Grocy.Components.LocationPicker.SetId(stockEntry.location_id); @@ -79,12 +79,12 @@ $('#save-stockedit-button').on('click', function(e) var bookingResponse = null; var stockRowId = GetUriParam('stockRowId'); - jsonData.stock_row_id = stockRowId; + jsonData.id = stockRowId; Grocy.Api.Put("stock", jsonData, function(result) { - var successMessage = __t('Stock entry successfully updated') + '
' + __t("Undo") + ''; + var successMessage = __t('Stock entry successfully updated') + '
' + __t("Undo") + ''; window.parent.postMessage(WindowMessageBag("StockDetailChanged", stockRowId), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); @@ -135,17 +135,3 @@ if (Grocy.Components.DateTimePicker) Grocy.FrontendHelpers.ValidateForm('stockedit-form'); }); } - -function UndoStockBooking(bookingId) -{ - Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { }, - function(result) - { - toastr.success(__t("Booking successfully undone")); - }, - function(xhr) - { - console.error(xhr); - } - ); -}; diff --git a/routes.php b/routes.php index 9f4ae346..679b11fc 100644 --- a/routes.php +++ b/routes.php @@ -161,8 +161,9 @@ $app->group('/api', function() if (GROCY_FEATURE_FLAG_STOCK) { $this->get('/stock', '\Grocy\Controllers\StockApiController:CurrentStock'); + $this->get('/stock/{entryId}/entry', '\Grocy\Controllers\StockApiController:StockEntry'); $this->put('/stock', '\Grocy\Controllers\StockApiController:EditStock'); - $this->get('/stock/volatile', '\Grocy\Controllers\StockApiController:CurrentVolatilStock'); + $this->get('/stock/volatile', '\Grocy\Controllers\StockApiController:CurrentVolatileStock'); $this->get('/stock/products/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails'); $this->get('/stock/products/{productId}/entries', '\Grocy\Controllers\StockApiController:ProductStockEntries'); $this->get('/stock/products/{productId}/locations', '\Grocy\Controllers\StockApiController:ProductStockLocations'); diff --git a/services/StockService.php b/services/StockService.php index efde1419..70b8cb5c 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -170,6 +170,11 @@ class StockService extends BaseService return $returnData; } + public function GetStockEntry($entryId) + { + return $this->Database->stock()->where('id', $entryId)->fetch(); + } + public function GetProductStockEntries($productId, $excludeOpened = false) { // In order of next use: diff --git a/views/stockdetail.blade.php b/views/stockdetail.blade.php index 344bbec4..53b6a146 100644 --- a/views/stockdetail.blade.php +++ b/views/stockdetail.blade.php @@ -127,10 +127,13 @@ {{ $__t('Edit product') }} - {{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }} From cdbfc3c3db3e5aef842dd181832000cef542ba92 Mon Sep 17 00:00:00 2001 From: kriddles <54413450+kriddles@users.noreply.github.com> Date: Fri, 17 Jan 2020 11:06:33 -0600 Subject: [PATCH 011/134] productcard.js check null location (#494) --- public/viewjs/components/productcard.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index f181127f..defd3a80 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -15,7 +15,10 @@ Grocy.Components.ProductCard.Refresh = function(productId) $('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || '2999-12-31'); $('#productcard-product-last-used').text((productDetails.last_used || '2999-12-31').substring(0, 10)); $('#productcard-product-last-used-timeago').attr("datetime", productDetails.last_used || '2999-12-31'); - $('#productcard-product-location').text(productDetails.location.name); + if (productDetails.location != null) + { + $('#productcard-product-location').text(productDetails.location.name); + } $('#productcard-product-spoil-rate').text((parseFloat(productDetails.spoil_rate_percent) / 100).toLocaleString(undefined, { style: "percent" })); if (productDetails.is_aggregated_amount == 1) From 5de563f2c941a97cc59f763dc3d9fe7f99dc321f Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 17 Jan 2020 18:08:54 +0100 Subject: [PATCH 012/134] Added changelog for #489 --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 57b43652..523d7570 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -7,6 +7,7 @@ ### Stock fixes - Fixed that entering partial amounts was not possible on the inventory page (only applies if the product option "Allow partial units in stock" is enabled) - Fixed that on purchase a wrong minimum amount was enforced for products with enabled tare weight handling in combination with different purchase/stock quantity units +- Fixed that the productcard did not load correctly when `FEATURE_FLAG_STOCK_LOCATION_TRACKING` was set to `false` (thanks @kriddles) ### Shopping list fixes - Fixed that when `FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS` was set to `false`, the shopping list appeared empty after some actions From 7c2320e9789c7e5c056dc786ab485407dfdfa06f Mon Sep 17 00:00:00 2001 From: kriddles <54413450+kriddles@users.noreply.github.com> Date: Fri, 17 Jan 2020 11:13:43 -0600 Subject: [PATCH 013/134] refresh productcard on save (#495) --- public/viewjs/consume.js | 1 + public/viewjs/inventory.js | 1 + public/viewjs/purchase.js | 1 + public/viewjs/transfer.js | 1 + 4 files changed, 4 insertions(+) diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 2b37f23c..18d9f808 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -130,6 +130,7 @@ $('#save-consume-button').on('click', function(e) $("#location_id").find("option").remove().end().append(""); } Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('consume-form'); } }, diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 1c044519..69195c0f 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -88,6 +88,7 @@ Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('inventory-form'); } }, diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 368264cc..51270ac1 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -99,6 +99,7 @@ Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('purchase-form'); } }, diff --git a/public/viewjs/transfer.js b/public/viewjs/transfer.js index f6fdcb66..88d2d111 100644 --- a/public/viewjs/transfer.js +++ b/public/viewjs/transfer.js @@ -114,6 +114,7 @@ $('#save-transfer-button').on('click', function(e) $("#location_id_to").val(""); $("#location_id_from").val(""); Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('transfer-form'); } }, From cd522220ce152e38c873383871bfbde24d5b290b Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Fri, 17 Jan 2020 18:15:45 +0100 Subject: [PATCH 014/134] Added changelog for #491 --- changelog/55_UNRELEASED_2019-xx-xx.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 523d7570..d2c66e23 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -4,7 +4,8 @@ - From there you can also edit the stock entries - A huge THANK YOU goes to @kriddles for the work on this feature -### Stock fixes +### Stock improvements/fixes +- The productcard gets now also refreshed after a transaction was posted (purchase/consume/etc.) (thanks @kriddles) - Fixed that entering partial amounts was not possible on the inventory page (only applies if the product option "Allow partial units in stock" is enabled) - Fixed that on purchase a wrong minimum amount was enforced for products with enabled tare weight handling in combination with different purchase/stock quantity units - Fixed that the productcard did not load correctly when `FEATURE_FLAG_STOCK_LOCATION_TRACKING` was set to `false` (thanks @kriddles) From 61a45c030f451a069975ba9c3486198c2c273290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20SACRE?= Date: Sun, 19 Jan 2020 09:14:07 +0100 Subject: [PATCH 015/134] Feature request : api/stock can return detailed products #487 (#503) The response of the call '/api/stock' now returns a new attribute ('product') which contains the details of the related product. --- grocy.openapi.json | 7 +++++-- services/StockService.php | 43 +++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/grocy.openapi.json b/grocy.openapi.json index 82480d78..d10a4b4a 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -163,7 +163,7 @@ { "$ref": "#/components/schemas/StockEntry" } - ] + ] } } } @@ -626,7 +626,7 @@ } } } - }, + }, "/files/{group}/{fileName}": { "get": { "summary": "Serves the given file", @@ -4065,6 +4065,9 @@ "is_aggregated_amount": { "type": "boolean", "description": "Indicates wheter this product has sub-products or not / if the fields `amount_aggregated` and `amount_opened_aggregated` are filled" + }, + "product": { + "$ref": "#/components/schemas/Product" } } }, diff --git a/services/StockService.php b/services/StockService.php index 70b8cb5c..57808a61 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -26,8 +26,25 @@ class StockService extends BaseService $sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)'; } - - return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); + + $current_stock = $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); + $stock_array = []; + + foreach ($current_stock as $cur) { + $stock_array[$cur->product_id] = $cur; + } + + $list_of_product_ids = implode(",", array_keys($stock_array)); + + $sql = 'SELECT * FROM products WHERE id in (' . $list_of_product_ids . ')'; + + $products = $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); + + foreach ($products as $product) { + $stock_array[$product->id]->product = $product; + } + + return $stock_array; } public function GetCurrentStockLocationContent() @@ -116,7 +133,7 @@ class StockService extends BaseService $quStock = $this->Database->quantity_units($product->qu_id_stock); $location = $this->Database->locations($product->location_id); $averageShelfLifeDays = intval($this->Database->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); - + $lastPrice = null; $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); if ($lastLogRow !== null && !empty($lastLogRow)) @@ -213,20 +230,20 @@ class StockService extends BaseService { throw new \Exception('The amount cannot be lower or equal than the defined tare weight + current stock amount'); } - + $amount = $amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight); } - + //Sets the default best before date, if none is supplied if ($bestBeforeDate == null) { if (intval($productDetails->product->default_best_before_days) == -1) { - $bestBeforeDate = date('2999-12-31'); + $bestBeforeDate = date('2999-12-31'); } else if (intval($productDetails->product->default_best_before_days) > 0) { - $bestBeforeDate = date('Y-m-d', strtotime(date('Y-m-d') . ' + '.$productDetails->product->default_best_before_days.' days')); + $bestBeforeDate = date('Y-m-d', strtotime(date('Y-m-d') . ' + '.$productDetails->product->default_best_before_days.' days')); } else { @@ -240,7 +257,7 @@ class StockService extends BaseService { $transactionId = uniqid(); } - + $stockId = uniqid(); $logRow = $this->Database->stock_log()->createRow(array( @@ -299,7 +316,7 @@ class StockService extends BaseService { throw new \Exception('The amount cannot be lower than the defined tare weight'); } - + $amount = abs($amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight)); } @@ -426,7 +443,7 @@ class StockService extends BaseService { throw new \Exception('The amount cannot be lower than the defined tare weight'); } - + $amount = abs($amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight)); } @@ -631,7 +648,7 @@ class StockService extends BaseService { $containerWeight = floatval($productDetails->product->tare_weight); } - + if ($newAmount == floatval($productDetails->stock_amount) + $containerWeight) { throw new \Exception('The new amount cannot equal the current stock amount'); @@ -643,7 +660,7 @@ class StockService extends BaseService { $bookingAmount = $newAmount; } - + return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId); } else if ($newAmount < $productDetails->stock_amount + $containerWeight) @@ -824,7 +841,7 @@ class StockService extends BaseService { $productRow->update(array('amount' => $newAmount)); } - + } } From d0036e8034fd3daf208894bfde5151e786250a73 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 19 Jan 2020 09:52:23 +0100 Subject: [PATCH 016/134] Tried to simplify #503 (also references #487) --- services/StockService.php | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/services/StockService.php b/services/StockService.php index 57808a61..a7dded39 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -26,25 +26,16 @@ class StockService extends BaseService $sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)'; } - - $current_stock = $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); - $stock_array = []; - - foreach ($current_stock as $cur) { - $stock_array[$cur->product_id] = $cur; + $currentStockMapped = $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_GROUP|\PDO::FETCH_OBJ); + + $relevantProducts = $this->Database->products()->where('id IN (SELECT product_id FROM (' . $sql . ') x)'); + foreach ($relevantProducts as $product) + { + $currentStockMapped[$product->id][0]->product_id = $product->id; + $currentStockMapped[$product->id][0]->product = $product; } - $list_of_product_ids = implode(",", array_keys($stock_array)); - - $sql = 'SELECT * FROM products WHERE id in (' . $list_of_product_ids . ')'; - - $products = $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); - - foreach ($products as $product) { - $stock_array[$product->id]->product = $product; - } - - return $stock_array; + return array_column($currentStockMapped, 0); } public function GetCurrentStockLocationContent() From 17e5c04bf975f161b04528c853aae91f362294e2 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 19 Jan 2020 09:53:58 +0100 Subject: [PATCH 017/134] Added changelog for #503 --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index d2c66e23..1135570f 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -26,6 +26,7 @@ - Fixed that a due date was required when editing an existing task ### API improvements/fixes +- The endpoint `/stock` now includes also the product object itself (new field/property `product`) (thanks @gsacre) - Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002) - Fixed the response type description of the `/stock/volatile` endpoint - New endpoints for the stock transfer & stock entry edit capabilities mentioned above From f543a3a472b444ce1b752a9f664710398b740ed1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 21 Jan 2020 17:30:09 +0100 Subject: [PATCH 018/134] Fixed and optimized some things related to #421 (& some more cleanup) --- controllers/StockController.php | 2 - localization/strings.pot | 3 + public/js/grocy.js | 8 +- public/viewjs/consume.js | 40 ++++--- public/viewjs/stockdetail.js | 191 +++++++++++--------------------- public/viewjs/stockedit.js | 108 +++++++++--------- public/viewjs/stockoverview.js | 122 +------------------- public/viewjs/transfer.js | 40 +++---- views/stockdetail.blade.php | 80 +++++-------- views/stockedit.blade.php | 22 +++- views/stockoverview.blade.php | 26 ++--- 11 files changed, 213 insertions(+), 429 deletions(-) diff --git a/controllers/StockController.php b/controllers/StockController.php index 7436d02d..e06a07ca 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -49,9 +49,7 @@ class StockController extends BaseController 'locations' => $this->Database->locations()->orderBy('name'), 'currentStockDetail' => $this->Database->stock()->orderBy('product_id'), 'currentStockLocations' => $this->StockService->GetCurrentStockLocations(), - 'missingProducts' => $this->StockService->GetMissingProducts(), 'nextXDays' => $nextXDays, - 'productGroups' => $this->Database->product_groups()->orderBy('name'), 'userfields' => $this->UserfieldsService->GetFields('products'), 'userfieldValues' => $this->UserfieldsService->GetAllValues('products') ]); diff --git a/localization/strings.pot b/localization/strings.pot index 408f8b77..2d9f91eb 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1615,3 +1615,6 @@ msgstr "" msgid "Keep screen on while displaying a \"fullscreen-card\"" msgstr "" + +msgid "A purchased date is required" +msgstr "" diff --git a/public/js/grocy.js b/public/js/grocy.js index 7ee410ad..cb080592 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -614,10 +614,10 @@ $(document).on("click", ".show-as-dialog-link", function(e) backdrop: true, closeButton: false, buttons: { - ok: { - label: __t('OK'), - className: 'btn-success responsive-button', - callback: function() + cancel: { + label: __t('Cancel'), + className: 'btn-secondary responsive-button', + callback: function () { bootbox.hideAll(); } diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 18d9f808..9faf4cca 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -1,24 +1,4 @@ -$(document).ready(function() { - - if (GetUriParam("embedded") !== undefined) - { - var locationId = GetUriParam('locationId'); - - if (typeof locationId === 'undefined') - { - Grocy.Components.ProductPicker.GetPicker().trigger('change'); - Grocy.Components.ProductPicker.GetInputElement().focus(); - } else { - - $("#location_id").val(locationId); - $("#location_id").trigger('change'); - $("#use_specific_stock_entry").click(); - $("#use_specific_stock_entry").trigger('change'); - } - } -}); - -$('#save-consume-button').on('click', function(e) +$('#save-consume-button').on('click', function(e) { e.preventDefault(); @@ -497,3 +477,21 @@ function UndoStockTransaction(transactionId) } ); }; + +if (GetUriParam("embedded") !== undefined) +{ + var locationId = GetUriParam('locationId'); + + if (typeof locationId === 'undefined') + { + Grocy.Components.ProductPicker.GetPicker().trigger('change'); + Grocy.Components.ProductPicker.GetInputElement().focus(); + } + else + { + $("#location_id").val(locationId); + $("#location_id").trigger('change'); + $("#use_specific_stock_entry").click(); + $("#use_specific_stock_entry").trigger('change'); + } +} diff --git a/public/viewjs/stockdetail.js b/public/viewjs/stockdetail.js index 2ee665c2..0772bd4a 100644 --- a/public/viewjs/stockdetail.js +++ b/public/viewjs/stockdetail.js @@ -7,45 +7,19 @@ }); $('#stock-detail-table tbody').removeClass("d-none"); -function bootBoxModal(message) { - bootbox.dialog({ - message: message, - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -} +$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) +{ + var productId = Grocy.Components.ProductPicker.GetValue(); + if ((isNaN(productId) || productId == "" || productId == data[1])) + { + return true; + } + + return false; +}); - -$.fn.dataTable.ext.search.push( - function( settings, data, dataIndex ) { - var productId = Grocy.Components.ProductPicker.GetValue(); - - if ( ( isNaN( productId ) || - productId == "" || - //assume productId is in the first column - productId == data[1] ) ) - { - return true; - } - return false; - } -); - -$(document).ready(function() { - Grocy.Components.ProductPicker.GetPicker().trigger('change'); -} ); +Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { @@ -114,29 +88,17 @@ $(document).on('click', '.product-open-button', function(e) var productId = $(e.currentTarget).attr('data-product-id'); var productName = $(e.currentTarget).attr('data-product-name'); var productQuName = $(e.currentTarget).attr('data-product-qu-name'); + var specificStockEntryId = $(e.currentTarget).attr('data-stock-id'); + var stockRowId = $(e.currentTarget).attr('data-stockrow-id'); var button = $(e.currentTarget); - - Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1 }, + + Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1, 'stock_entry_id': specificStockEntryId }, function(bookingResponse) { - Grocy.Api.Get('stock/products/' + productId, - function(result) - { - if (result.stock_amount == result.stock_amount_opened) - { - button.addClass("disabled"); - } - - Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + ''); - RefreshProductRow(productId); - }, - function(xhr) - { - Grocy.FrontendHelpers.EndUiBusy(); - console.error(xhr); - } - ); + button.addClass("disabled"); + Grocy.FrontendHelpers.EndUiBusy(); + toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + ''); + RefreshStockDetailRow(stockRowId); }, function(xhr) { @@ -152,91 +114,40 @@ $(document).on("click", ".stock-name-cell", function(e) $("#stockdetail-productcard-modal").modal("show"); }); -$(document).on("click", ".product-purchase-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootBoxModal(''); -}); - -$(document).on("click", ".product-transfer-button", function(e) -{ - - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - var locationId = $(e.currentTarget).attr('data-location-id'); - var specificStockEntryId = $(e.currentTarget).attr('data-stock-id'); - bootBoxModal(''); - -}); - -$(document).on("click", ".product-consume-custom-amount-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - var locationId = $(e.currentTarget).attr('data-location-id'); - var specificStockEntryId = $(e.currentTarget).attr('data-stock-id'); - - bootBoxModal(''); - -}); - -$(document).on("click", ".product-inventory-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootBoxModal(''); -}); - -$(document).on("click", ".product-stockedit-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - var stockRowId = $(e.currentTarget).attr("data-id"); - - bootBoxModal(''); -}); - -$(document).on("click", ".product-add-to-shopping-list-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootBoxModal(''); -}); - function RefreshStockDetailRow(stockRowId) { Grocy.Api.Get("stock/" + stockRowId + "/entry", function(result) { var stockRow = $('#stock-' + stockRowId + '-row'); - var now = moment(); - - stockRow.removeClass("table-warning"); - stockRow.removeClass("table-danger"); - stockRow.removeClass("table-info"); - stockRow.removeClass("d-none"); - stockRow.removeAttr("style"); - + if (result == null || result.amount == 0) { stockRow.fadeOut(500, function() { - //$(this).tooltip("hide"); $(this).addClass("d-none"); }); } else { + var expiringThreshold = moment().add(Grocy.UserSettings.stock_expring_soon_days, "days"); + var now = moment(); + var bestBeforeDate = moment(result.best_before_date); + + stockRow.removeClass("table-warning"); + stockRow.removeClass("table-danger"); + stockRow.removeClass("table-info"); + stockRow.removeClass("d-none"); + stockRow.removeAttr("style"); + if (now.isAfter(bestBeforeDate)) + { + stockRow.addClass("table-danger"); + } + else if (bestBeforeDate.isBefore(expiringThreshold)) + { + stockRow.addClass("table-warning"); + } + $('#stock-' + stockRowId + '-amount').parent().effect('highlight', { }, 500); $('#stock-' + stockRowId + '-amount').fadeOut(500, function () { @@ -248,6 +159,7 @@ function RefreshStockDetailRow(stockRowId) { $(this).text(result.best_before_date).fadeIn(500); }); + $('#stock-' + stockRowId + '-best-before-date-timeago').attr('datetime', result.best_before_date + ' 23:59:59'); var locationName = ""; Grocy.Api.Get("objects/locations/" + result.location_id, @@ -258,7 +170,8 @@ function RefreshStockDetailRow(stockRowId) function(xhr) { console.error(xhr); - }); + } + ); $('#stock-' + stockRowId + '-location').parent().effect('highlight', { }, 500); $('#stock-' + stockRowId + '-location').fadeOut(500, function() { @@ -276,8 +189,24 @@ function RefreshStockDetailRow(stockRowId) { $(this).text(result.purchased_date).fadeIn(500); }); + $('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59'); + + $('#stock-' + stockRowId + '-opened-amount').parent().effect('highlight', {}, 500); + $('#stock-' + stockRowId + '-opened-amount').fadeOut(500, function () + { + if (result.open == 1) + { + $(this).text(__t('Opened')).fadeIn(500); + } + else + { + $(this).text("").fadeIn(500); + $(".product-open-button[data-stockrow-id='" + stockRowId + "']").removeClass("disabled"); + } + }); } + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... setTimeout(function() { RefreshContextualTimeago(); @@ -316,3 +245,9 @@ function UndoStockBookingEntry(bookingId, stockRowId) } ); }; + +$(document).on("click", ".product-name-cell", function(e) +{ + Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id")); + $("#productcard-modal").modal("show"); +}); diff --git a/public/viewjs/stockedit.js b/public/viewjs/stockedit.js index ca02abb2..109d092a 100644 --- a/public/viewjs/stockedit.js +++ b/public/viewjs/stockedit.js @@ -1,57 +1,4 @@ -$(document).ready(function() { - var stockRowId = GetUriParam('stockRowId'); - Grocy.Api.Get("stock/" + stockRowId + "/entry", - function(stockEntry) - { - Grocy.Components.LocationPicker.SetId(stockEntry.location_id); - $('#amount').val(stockEntry.amount); - $('#price').val(stockEntry.price); - Grocy.Components.DateTimePicker.SetValue(stockEntry.best_before_date); - - Grocy.Api.Get('stock/products/' + stockEntry.product_id, - function(productDetails) - { - $('#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(__t('The amount cannot be lower than %1$s', 0.01.toLocaleString())); - } - else - { - $("#amount").attr("min", "1"); - $("#amount").attr("step", "1"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', '1')); - } - - if (productDetails.product.enable_tare_weight_handling == 1) - { - $("#amount").attr("min", productDetails.product.tare_weight); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', parseFloat(productDetails.product.tare_weight).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }))); - $("#tare-weight-handling-info").removeClass("d-none"); - } - else - { - $("#tare-weight-handling-info").addClass("d-none"); - } - - }, - function(xhr) - { - console.error(xhr); - } - ); - }, - function(xhr) - { - console.error(xhr); - } - ); -} ); - -$('#save-stockedit-button').on('click', function(e) +$('#save-stockedit-button').on('click', function(e) { e.preventDefault(); @@ -76,8 +23,6 @@ $('#save-stockedit-button').on('click', function(e) } jsonData.price = price; - var bookingResponse = null; - var stockRowId = GetUriParam('stockRowId'); jsonData.id = stockRowId; @@ -135,3 +80,54 @@ if (Grocy.Components.DateTimePicker) Grocy.FrontendHelpers.ValidateForm('stockedit-form'); }); } + +var stockRowId = GetUriParam('stockRowId'); +Grocy.Api.Get("stock/" + stockRowId + "/entry", + function (stockEntry) + { + Grocy.Components.LocationPicker.SetId(stockEntry.location_id); + $('#amount').val(stockEntry.amount); + $('#price').val(stockEntry.price); + Grocy.Components.DateTimePicker.SetValue(stockEntry.best_before_date); + + Grocy.Api.Get('stock/products/' + stockEntry.product_id, + function (productDetails) + { + $('#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(__t('The amount cannot be lower than %1$s', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', '1')); + } + + if (productDetails.product.enable_tare_weight_handling == 1) + { + $("#amount").attr("min", productDetails.product.tare_weight); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', parseFloat(productDetails.product.tare_weight).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }))); + $("#tare-weight-handling-info").removeClass("d-none"); + } + else + { + $("#tare-weight-handling-info").addClass("d-none"); + } + + }, + function (xhr) + { + console.error(xhr); + } + ); + }, + function (xhr) + { + console.error(xhr); + } +); diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 4047f197..c3d26bba 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -196,126 +196,6 @@ function RefreshStatistics() } RefreshStatistics(); -$(document).on("click", ".product-purchase-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootbox.dialog({ - message: '', - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -}); - -$(document).on("click", ".product-transfer-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootbox.dialog({ - message: '', - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -}); - -$(document).on("click", ".product-consume-custom-amount-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootbox.dialog({ - message: '', - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -}); - -$(document).on("click", ".product-inventory-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootbox.dialog({ - message: '', - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -}); - -$(document).on("click", ".product-add-to-shopping-list-button", function(e) -{ - e.preventDefault(); - - var productId = $(e.currentTarget).attr("data-product-id"); - - bootbox.dialog({ - message: '', - size: 'large', - backdrop: true, - closeButton: false, - buttons: { - cancel: { - label: __t('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - bootbox.hideAll(); - } - } - } - }); -}); - function RefreshProductRow(productId) { productId = productId.toString(); @@ -398,7 +278,7 @@ function RefreshProductRow(productId) { $(this).text(result.next_best_before_date).fadeIn(500); }); - $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); + $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date + ' 23:59:59'); if (result.stock_amount_opened > 0) { diff --git a/public/viewjs/transfer.js b/public/viewjs/transfer.js index 88d2d111..1918befa 100644 --- a/public/viewjs/transfer.js +++ b/public/viewjs/transfer.js @@ -1,23 +1,4 @@ -$(document).ready(function() { - if (GetUriParam("embedded") !== undefined) - { - var locationId = GetUriParam('locationId'); - - if (typeof locationId === 'undefined') - { - Grocy.Components.ProductPicker.GetPicker().trigger('change'); - Grocy.Components.ProductPicker.GetInputElement().focus(); - } else { - - $("#location_id_from").val(locationId); - $("#location_id_from").trigger('change'); - $("#use_specific_stock_entry").click(); - $("#use_specific_stock_entry").trigger('change'); - } - } -}); - -$('#save-transfer-button').on('click', function(e) +$('#save-transfer-button').on('click', function(e) { e.preventDefault(); @@ -440,3 +421,22 @@ function UndoStockTransaction(transactionId) } ); }; + +if (GetUriParam("embedded") !== undefined) +{ + var locationId = GetUriParam('locationId'); + + if (typeof locationId === 'undefined') + { + Grocy.Components.ProductPicker.GetPicker().trigger('change'); + Grocy.Components.ProductPicker.GetInputElement().focus(); + } + else + { + + $("#location_id_from").val(locationId); + $("#location_id_from").trigger('change'); + $("#use_specific_stock_entry").click(); + $("#use_specific_stock_entry").trigger('change'); + } +} diff --git a/views/stockdetail.blade.php b/views/stockdetail.blade.php index 53b6a146..1b11535b 100644 --- a/views/stockdetail.blade.php +++ b/views/stockdetail.blade.php @@ -9,21 +9,16 @@ @endpush -@push('pageStyles') - -@endpush - @section('content')

@yield('title')

- @include('components.productpicker', array('products' => $products,'disallowAddProductWorkflows' => true)) + @include('components.productpicker', array( + 'products' => $products, + 'disallowAllProductWorkflows' => true + ))
@@ -48,19 +43,9 @@ @foreach($currentStockDetail as $currentStockEntry) - amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif"> + amount > 0) table-warning @endif"> - - 1 - - - {{ $__t('All') }} + @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) - - 1 + data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}" + data-stock-id="{{ $currentStockEntry->stock_id }}" + data-stockrow-id="{{ $currentStockEntry->id }}"> + + + + @endif -'); if (productDetails.product.picture_file_name && !productDetails.product.picture_file_name.isEmpty()) @@ -500,18 +501,19 @@ $(document).on('click', '.product-consume-button', function(e) Grocy.FrontendHelpers.BeginUiBusy(); var productId = $(e.currentTarget).attr('data-product-id'); - var consumeAmount = $(e.currentTarget).attr('data-product-amount'); + var consumeAmount = parseFloat($(e.currentTarget).attr('data-product-amount')); Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': false }, function(bookingResponse) { Grocy.Api.Get('stock/products/' + productId, - function(result) + function (result) { var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; Grocy.FrontendHelpers.EndUiBusy(); toastr.success(toastMessage); + window.location.reload(); }, function(xhr) { From 98f70d1525eedfb2fb4b6a4fdd8f4823167d2989 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 25 Jan 2020 18:36:54 +0100 Subject: [PATCH 047/134] Finalize products on meal plan feature (references #450) --- public/viewjs/mealplan.js | 2 +- public/viewjs/shoppinglistitemform.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 8e2c08ea..7801973d 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -213,7 +213,7 @@ var calendar = $("#calendar").fullCalendar({
\ \ \ - \ + \
\ '); diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js index 8cbc2530..d7a5103e 100644 --- a/public/viewjs/shoppinglistitemform.js +++ b/public/viewjs/shoppinglistitemform.js @@ -188,3 +188,9 @@ if (GetUriParam("list") !== undefined) { $("#shopping_list_id").val(GetUriParam("list")); } + +if (GetUriParam("amount") !== undefined) +{ + $("#amount").val(GetUriParam("amount")); + Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); +} From d64a1a546c4cd773b38e7f7a6c935687ebc40e4f Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 25 Jan 2020 19:42:46 +0100 Subject: [PATCH 048/134] Finalize products on meal plan feature (references #450) --- public/viewjs/mealplan.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 7801973d..4d465037 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -172,8 +172,7 @@ var calendar = $("#calendar").fullCalendar({ } element.attr("data-product-details", event.productDetails); - console.log(productDetails); - console.log(mealPlanEntry); + var productOrderMissingButtonDisabledClasses = "disabled"; if (parseFloat(productDetails.stock_amount_aggregated) < parseFloat(mealPlanEntry.product_amount)) { @@ -197,7 +196,7 @@ var calendar = $("#calendar").fullCalendar({ var costsAndCaloriesPerServing = "" if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) { - costsAndCaloriesPerServing = '
' + productDetails.last_price * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '
'; + costsAndCaloriesPerServing = '
' + productDetails.last_price / productDetails.product.qu_factor_purchase_to_stock * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '
'; } else { From 0ef9b2fdb7ec1f43e3b876343b5370c9bb1af89d Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 25 Jan 2020 20:01:40 +0100 Subject: [PATCH 049/134] Added a new setting to be able to start the meal plan on a different weekday (closes #429) --- changelog/55_UNRELEASED_2019-xx-xx.md | 1 + config-dist.php | 7 ++++++- public/viewjs/mealplan.js | 4 ++++ views/mealplan.blade.php | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 7904e63a..7c759c30 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -31,6 +31,7 @@ - It's now possible to products directly (also in the dropdown of the add button in the header of each day column, maybe useful in combination with the new "Self produced products" feature) - Added that the calories per serving are now also shown - Added that the total costs and calories per day are displayed in the header of each day column +- Added new `config.php` setting `MEAL_PLAN_FIRST_DAY_OF_WEEK` which can be used to start the meal plan on a different day (defaults to `CALENDAR_FIRST_DAY_OF_WEEK`, so no changed behavior when not configured) - Fixed that when `FEATURE_FLAG_STOCK_PRICE_TRACKING` was set to `false`, prices were still shown (thanks @kriddles) - Fixed that the week costs were missing for the weeks 1 - 9 of a year diff --git a/config-dist.php b/config-dist.php index bf2ca90a..4884ff7c 100644 --- a/config-dist.php +++ b/config-dist.php @@ -33,7 +33,7 @@ Setting('CALENDAR_SHOW_WEEK_OF_YEAR', true); # To keep it simple: grocy does not handle any currency conversions, # this here is used to format all money values, -# so doesn't matter really matter, but should be the +# so doesn't really matter, but should be the # ISO 4217 code of the currency ("USD", "EUR", "GBP", etc.) Setting('CURRENCY', 'USD'); @@ -62,6 +62,11 @@ Setting('DISABLE_AUTH', false); # Set this to true if you want to disable the ability to scan a barcode via the device camera (Browser API) Setting('DISABLE_BROWSER_BARCODE_CAMERA_SCANNING', false); +# Set this if you want to have a different start day for the weekly meal plan view, +# leave empty to use CALENDAR_FIRST_DAY_OF_WEEK (see above) +# Needs to be a number where Sunday = 0, Monday = 1 and so forth +Setting('MEAL_PLAN_FIRST_DAY_OF_WEEK', ''); + # Default user settings # These settings can be changed per user, here the defaults diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 4d465037..18785fa9 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -5,6 +5,10 @@ if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) { firstDay = parseInt(Grocy.CalendarFirstDayOfWeek); } +if (!Grocy.MealPlanFirstDayOfWeek.isEmpty()) +{ + firstDay = parseInt(Grocy.MealPlanFirstDayOfWeek); +} var calendar = $("#calendar").fullCalendar({ "themeSystem": "bootstrap4", diff --git a/views/mealplan.blade.php b/views/mealplan.blade.php index 5eec2e83..cc2385cf 100644 --- a/views/mealplan.blade.php +++ b/views/mealplan.blade.php @@ -21,6 +21,8 @@ Grocy.QuantityUnits = {!! json_encode($quantityUnits) !!}; Grocy.QuantityUnitConversionsResolved = {!! json_encode($quantityUnitConversionsResolved) !!}; + + Grocy.MealPlanFirstDayOfWeek = '{{ GROCY_MEAL_PLAN_FIRST_DAY_OF_WEEK }}';
From ac1be1e90f42ff430dda44f6d3614785e15e9868 Mon Sep 17 00:00:00 2001 From: kriddles <54413450+kriddles@users.noreply.github.com> Date: Sun, 26 Jan 2020 01:50:44 -0600 Subject: [PATCH 050/134] 450 updates (#518) * prevent seeing undefinde if no recipes * disable weekRecipeConsume if weekCosts are zero * reword title * Add meal plan notes and products to Calendar --- public/viewjs/mealplan.js | 7 ++++--- services/CalendarService.php | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 18785fa9..960ec7df 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -49,9 +49,9 @@ var calendar = $("#calendar").fullCalendar({ var weekCosts = 0; var weekRecipeOrderMissingButtonHtml = ""; var weekRecipeConsumeButtonHtml = ""; + var weekCostsHtml = ""; if (weekRecipe !== null) { - var weekCostsHtml = ""; if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) { weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs; @@ -64,12 +64,13 @@ var calendar = $("#calendar").fullCalendar({ weekRecipeOrderMissingButtonDisabledClasses = "disabled"; } var weekRecipeConsumeButtonDisabledClasses = ""; - if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0) + console.log(weekCosts); + if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0 || weekCosts == 0) { weekRecipeConsumeButtonDisabledClasses = "disabled"; } weekRecipeOrderMissingButtonHtml = '' - weekRecipeConsumeButtonHtml = '' + weekRecipeConsumeButtonHtml = '' } $(".fc-header-toolbar .fc-center").html("

" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "

"); }, diff --git a/services/CalendarService.php b/services/CalendarService.php index e951cb91..310fa117 100644 --- a/services/CalendarService.php +++ b/services/CalendarService.php @@ -92,7 +92,7 @@ class CalendarService extends BaseService $recipes = $this->Database->recipes(); $mealPlanDayRecipes = $this->Database->recipes()->where('type', 'mealplan-day'); - $titlePrefix = $this->LocalizationService->__t('Meal plan') . ': '; + $titlePrefix = $this->LocalizationService->__t('Meal plan recipe') . ': '; $mealPlanRecipeEvents = array(); foreach($mealPlanDayRecipes as $mealPlanDayRecipe) { @@ -108,6 +108,31 @@ class CalendarService extends BaseService } } - return array_merge($stockEvents, $taskEvents, $choreEvents, $batteryEvents, $mealPlanRecipeEvents); + $mealPlanDayNotes = $this->Database->meal_plan()->where('type', 'note'); + $titlePrefix = $this->LocalizationService->__t('Meal plan note') . ': '; + $mealPlanNotesEvents = array(); + foreach($mealPlanDayNotes as $mealPlanDayNote) + { + $mealPlanNotesEvents[] = array( + 'title' => $titlePrefix . $mealPlanDayNote->note, + 'start' => $mealPlanDayNote->day, + 'date_format' => 'date' + ); + } + + $products = $this->Database->products(); + $mealPlanDayProducts = $this->Database->meal_plan()->where('type', 'product'); + $titlePrefix = $this->LocalizationService->__t('Meal plan product') . ': '; + $mealPlanProductEvents = array(); + foreach($mealPlanDayProducts as $mealPlanDayProduct) + { + $mealPlanProductEvents[] = array( + 'title' => $titlePrefix . FindObjectInArrayByPropertyValue($products, 'id', $mealPlanDayProduct->product_id)->name, + 'start' => $mealPlanDayProduct->day, + 'date_format' => 'date' + ); + } + + return array_merge($stockEvents, $taskEvents, $choreEvents, $batteryEvents, $mealPlanRecipeEvents, $mealPlanNotesEvents, $mealPlanProductEvents); } } From 7a048136c64d55c1420223dd3b47e2b06adf9195 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 26 Jan 2020 13:40:26 +0100 Subject: [PATCH 051/134] Added missing localization strings --- localization/strings.pot | 12 ++++++++++++ public/viewjs/mealplan.js | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/localization/strings.pot b/localization/strings.pot index 96863ba1..8a3768e6 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1657,3 +1657,15 @@ msgstr "" msgid "Add product to %s" msgstr "" + +msgid "Consume all ingredients needed by this weeks recipes or products" +msgstr "" + +msgid "Meal plan recipe" +msgstr "" + +msgid "Meal plan note" +msgstr "" + +msgid "Meal plan product" +msgstr "" diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 960ec7df..dc3fff2f 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -63,8 +63,8 @@ var calendar = $("#calendar").fullCalendar({ { weekRecipeOrderMissingButtonDisabledClasses = "disabled"; } + var weekRecipeConsumeButtonDisabledClasses = ""; - console.log(weekCosts); if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0 || weekCosts == 0) { weekRecipeConsumeButtonDisabledClasses = "disabled"; From c7bcb9984a5ead1aac7d830150516d23a4d8f062 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 26 Jan 2020 15:35:01 +0100 Subject: [PATCH 052/134] Implemented "Scan mode" --- changelog/55_UNRELEASED_2019-xx-xx.md | 8 +++++ config-dist.php | 2 ++ localization/strings.pot | 12 +++++++ package.json | 1 + public/js/grocy.js | 2 +- public/js/grocy_uisound.js | 26 ++++++++++++++ public/uisounds/barcodescannerbeep.mp3 | Bin 0 -> 20781 bytes public/uisounds/error.mp3 | Bin 0 -> 6313 bytes public/uisounds/silence.mp3 | Bin 0 -> 9467 bytes public/uisounds/success.mp3 | Bin 0 -> 16344 bytes public/viewjs/consume.js | 45 ++++++++++++++++++++++-- public/viewjs/purchase.js | 39 ++++++++++++++++++++- public/viewjs/transfer.js | 3 ++ views/consume.blade.php | 46 ++++++++++++++++--------- views/purchase.blade.php | 14 +++++++- yarn.lock | 14 ++++++++ 16 files changed, 190 insertions(+), 22 deletions(-) create mode 100644 public/js/grocy_uisound.js create mode 100644 public/uisounds/barcodescannerbeep.mp3 create mode 100644 public/uisounds/error.mp3 create mode 100644 public/uisounds/silence.mp3 create mode 100644 public/uisounds/success.mp3 diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 7c759c30..28574d5f 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -4,6 +4,14 @@ - From there you can also edit the stock entries - A huge THANK YOU goes to @kriddles for the work on this feature +### New feature: Scan mode +- New switch-button on the purchase and consume page +- When enabled + - The amount will always be filled with `1` after changing/scanning a product + - If all fields could be automatically populated (means for purchase the product has a default best before date set), the transaction is automatically submitted + - If not, a warning is displayed and you can fill in the missing information + - Audio feedback is provided after scanning and on success/error of the transaction + ### New feature: Self produced products - To a recipe a product can be attached - This products needs a "Default best before date" diff --git a/config-dist.php b/config-dist.php index 4884ff7c..35f6d83f 100644 --- a/config-dist.php +++ b/config-dist.php @@ -91,6 +91,8 @@ DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for DefaultUserSetting('stock_expring_soon_days', 5); DefaultUserSetting('stock_default_purchase_amount', 0); DefaultUserSetting('stock_default_consume_amount', 1); +DefaultUserSetting('scan_mode_consume_enabled', false); +DefaultUserSetting('scan_mode_purchase_enabled', false); # Chores settings DefaultUserSetting('chores_due_soon_days', 5); diff --git a/localization/strings.pot b/localization/strings.pot index 8a3768e6..96ed6add 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1669,3 +1669,15 @@ msgstr "" msgid "Meal plan product" msgstr "" + +msgid "Scan mode" +msgstr "" + +msgid "on" +msgstr "" + +msgid "off" +msgstr "" + +msgid "Scan mode is on but not all required fields could be populated automatically" +msgstr "" diff --git a/package.json b/package.json index eaadff35..7709a79b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "bootbox": "^5.3.2", "bootstrap": "^4.3.1", "bootstrap-select": "^1.13.10", + "bootstrap-switch-button": "https://github.com/walidbagh/bootstrap-switch-button#Fix-module-export", "chart.js": "^2.8.0", "datatables.net": "^1.10.19", "datatables.net-bs4": "^1.10.19", diff --git a/public/js/grocy.js b/public/js/grocy.js index cb080592..08fbeab9 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -425,7 +425,7 @@ $(document).on("click", "select", function() }); // Auto saving user setting controls -$(".user-setting-control").on("change", function() +$(document).on("change", ".user-setting-control", function() { var element = $(this); var settingKey = element.attr("data-setting-key"); diff --git a/public/js/grocy_uisound.js b/public/js/grocy_uisound.js new file mode 100644 index 00000000..b7150084 --- /dev/null +++ b/public/js/grocy_uisound.js @@ -0,0 +1,26 @@ +Grocy.UISound = { }; + +Grocy.UISound.Play = function(url) +{ + new Audio(url).play(); +} + +Grocy.UISound.AskForPermission = function() +{ + Grocy.UISound.Play(U("/uisounds/silence.mp3")); +} + +Grocy.UISound.Success = function() +{ + Grocy.UISound.Play(U("/uisounds/success.mp3")); +} + +Grocy.UISound.Error = function() +{ + Grocy.UISound.Play(U("/uisounds/error.mp3")); +} + +Grocy.UISound.BarcodeScannerBeep = function() +{ + Grocy.UISound.Play(U("/uisounds/barcodescannerbeep.mp3")); +} diff --git a/public/uisounds/barcodescannerbeep.mp3 b/public/uisounds/barcodescannerbeep.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e488a47e8665a1a1df3c6229c3962a85deab7c24 GIT binary patch literal 20781 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 Fe*?7W+AshB literal 0 HcmV?d00001 diff --git a/public/uisounds/error.mp3 b/public/uisounds/error.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f68bc892c9d0f3b3f2c696d824c2ca15a7f10702 GIT binary patch literal 6313 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 literal 0 HcmV?d00001 diff --git a/public/uisounds/success.mp3 b/public/uisounds/success.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6c752d65a7822088695d14bafe6aaff850e9547c GIT binary patch literal 16344 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 wO{n>QDeMzEo4vOz|JTnr%"); if ($("#use_specific_stock_entry").is(":checked")) { @@ -265,13 +275,14 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) $('#amount_qu_unit').text(productDetails.quantity_unit_stock.name); $("#location_id").find("option").remove().end().append(""); - Grocy.Api.Get("stock/products/" + productId + '/locations', + Grocy.Api.Get("stock/products/" + productId + '/locations', function(stockLocations) { var setDefault = 0; stockLocations.forEach(stockLocation => { - if (productDetails.location.id == stockLocation.location_id) { + if (productDetails.location.id == stockLocation.location_id) + { $("#location_id").append($("