Merge remote-tracking branch 'origin' into grocycode

This commit is contained in:
Katharina Bogad 2021-06-11 18:54:19 +02:00
commit 18ad5a8f12
24 changed files with 280 additions and 76 deletions

View File

@ -21,10 +21,10 @@ Please don't send me private messages regarding grocy help. I check the issue tr
See the website for a list of community contributed Add-ons / Tools: [https://grocy.info/addons](https://grocy.info/addons) See the website for a list of community contributed Add-ons / Tools: [https://grocy.info/addons](https://grocy.info/addons)
## Motivation ## Motivation
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge! A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# Windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
## How to install ## How to install
> Checkout [grocy-desktop](https://github.com/grocy/grocy-desktop), if you want to run grocy without having to manage a webserver just like a normal ("indows) desktop application. > Checkout [grocy-desktop](https://github.com/grocy/grocy-desktop), if you want to run grocy without having to manage a webserver just like a normal (Windows) desktop application.
> >
> Directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next". > Directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next".

View File

@ -0,0 +1,29 @@
### Stock improvements/fixes
- Product barcodes are now enforced to be unique across products
- Fixed that editing stock entries was not possible
- Fixed that consuming with Scan Mode was not possible
- Fixed that the current stock total value (header of the stock overview page) didn't include decimal amounts (thanks @Ape)
- Fixed that the transfer page was not fully populated when opening it from the stock entries page
### Shopping list improvements/fixes
- The amount now defaults to `1` for adding items quicker
- Fixed that shopping list prints had a grey background (thanks @Forceu)
- Fixed the form validation on the shopping list item page (thanks @Forceu)
### Recipe improvements/fixes
- Recipe printing improvements (thanks @Ape)
- Calories are now always displayed per single serving (on the recipe and meal plan page)
- Fixed that "Only check if any amount is in stock" (recipe ingredient option) didn't work for stock amounts < 1
### Chores fixes
- Fixed that tracking chores with "Done by" a different user was not possible
### Userfield fixes
- Fixed that numeric Userfields were initialised with `1.0`
### General & other improvements/fixes
- Some night mode style improvements (thanks @BlizzWave and @KTibow)
- Fixed that the number picker up/down buttons did not work when the input field was empty or contained an invalid number
### API fixes
- Fixed that due soon products with `due_type` = "Expiration date" were missing in `due_products` of the `/stock/volatile` endpoint

View File

@ -25,5 +25,8 @@
"files": [ "files": [
"helpers/extensions.php" "helpers/extensions.php"
] ]
},
"config": {
"platform-check": false
} }
} }

View File

@ -124,7 +124,7 @@ DefaultUserSetting('stock_default_consume_amount_use_quick_consume_amount', fals
DefaultUserSetting('scan_mode_consume_enabled', false); DefaultUserSetting('scan_mode_consume_enabled', false);
DefaultUserSetting('scan_mode_purchase_enabled', false); DefaultUserSetting('scan_mode_purchase_enabled', false);
DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true); DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true);
DefaultUserSetting('show_purchased_date_on_purchase', false); // Wheter the purchased date should be editable on purchase (defaults to today otherwise) DefaultUserSetting('show_purchased_date_on_purchase', false); // Whether the purchased date should be editable on purchase (defaults to today otherwise)
DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock
// Shopping list settings // Shopping list settings
@ -201,4 +201,4 @@ Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true);
// Feature settings // Feature settings
Setting('FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT', true); // When set to true, opened items will be counted as missing for calculating if a product is below its minimum stock amount Setting('FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT', true); // When set to true, opened items will be counted as missing for calculating if a product is below its minimum stock amount
Setting('FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA', true); // Enables the torch automaticaly (if the device has one) Setting('FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA', true); // Enables the torch automatically (if the device has one)

View File

@ -157,18 +157,18 @@ class BaseController
if (GROCY_AUTHENTICATED) if (GROCY_AUTHENTICATED)
{ {
$this->View->set('permissions', User::PermissionList()); $this->View->set('permissions', User::PermissionList());
}
$decimalPlacesAmounts = intval($this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'stock_decimal_places_amounts')); $decimalPlacesAmounts = intval($this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'stock_decimal_places_amounts'));
if ($decimalPlacesAmounts <= 0) if ($decimalPlacesAmounts <= 0)
{ {
$defaultMinAmount = 1; $defaultMinAmount = 1;
}
else
{
$defaultMinAmount = '0.' . str_repeat('0', $decimalPlacesAmounts - 1) . '1';
}
$this->View->set('DEFAULT_MIN_AMOUNT', $defaultMinAmount);
} }
else
{
$defaultMinAmount = '0.' . str_repeat('0', $decimalPlacesAmounts - 1) . '1';
}
$this->View->set('DEFAULT_MIN_AMOUNT', $defaultMinAmount);
return $this->View->render($response, $page, $data); return $this->View->render($response, $page, $data);
} }

View File

@ -81,7 +81,7 @@ class ChoresApiController extends BaseApiController
if ($doneBy != GROCY_USER_ID) if ($doneBy != GROCY_USER_ID)
{ {
User::checkPermission($request, User::PERMISSION_CHORE_TRACK_EXECUTION_EXECUTION); User::checkPermission($request, User::PERMISSION_CHORE_TRACK_EXECUTION);
} }
$choreExecutionId = $this->getChoresService()->TrackChore($args['choreId'], $trackedTime, $doneBy); $choreExecutionId = $this->getChoresService()->TrackChore($args['choreId'], $trackedTime, $doneBy);

View File

@ -156,6 +156,11 @@ function BoolToString(bool $bool)
return $bool ? 'true' : 'false'; return $bool ? 'true' : 'false';
} }
function BoolToInt(bool $bool)
{
return $bool ? 1 : 0;
}
function ExternalSettingValue(string $value) function ExternalSettingValue(string $value)
{ {
$tvalue = rtrim($value, "\r\n"); $tvalue = rtrim($value, "\r\n");

View File

@ -87,7 +87,7 @@ abstract class AuthMiddleware extends BaseMiddleware
/** /**
* @param array $postParams * @param array $postParams
* @return bool True/False if the provided credentials were valid * @return bool True/False if the provided credentials were valid
* @throws \Exception Throws an \Exception if an error happended during credentials processing or if this AuthMiddleware doesn't provide credentials processing (e. g. handles this externally) * @throws \Exception Throws an \Exception if an error happened during credentials processing or if this AuthMiddleware doesn't provide credentials processing (e. g. handles this externally)
*/ */
abstract public static function ProcessLogin(array $postParams); abstract public static function ProcessLogin(array $postParams);

13
migrations/0128.sql Normal file
View File

@ -0,0 +1,13 @@
-- Duplicate product barcodes were most probably not created on purpose,
-- so just keep the newer one for any duplicates
DELETE FROM product_barcodes
WHERE id IN (
SELECT MIN(id)
FROM product_barcodes
GROUP BY barcode
HAVING COUNT(*) > 1
);
CREATE UNIQUE INDEX ix_product_barcodes ON product_barcodes (
barcode
);

99
migrations/0129.sql Normal file
View File

@ -0,0 +1,99 @@
DROP VIEW recipes_pos_resolved;
CREATE VIEW recipes_pos_resolved
AS
-- Multiplication by 1.0 to force conversion to float (REAL)
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled,
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount,
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
CASE WHEN IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * pop.price * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 0
UNION
-- Just add all recipe positions which should not be checked against stock with fulfilled need
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
1 AS need_fulfilled,
0 AS missing_amount,
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
1 AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * IFNULL(pop.price, 0) * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 1;

View File

@ -1,8 +1,12 @@
body.night-mode { body.night-mode {
color: #c1c1c1; color: #c1c1c1;
background-color: #333131; background-color: #333131;
} }
.night-mode .navbar-brand img {
filter: invert(0.9) hue-rotate(176deg);
}
.night-mode .table-info, .night-mode .table-info,
.night-mode .table-info > td, .night-mode .table-info > td,
.night-mode .table-info > th, .night-mode .table-info > th,
@ -12,7 +16,7 @@
} }
.night-mode .table { .night-mode .table {
color: #6c757d; color: #c1c1c1;
} }
.night-mode table.dataTable tr.dtrg-group td, .night-mode table.dataTable tr.dtrg-group td,
@ -24,6 +28,7 @@
.night-mode .btn, .night-mode .btn,
.night-mode .nav-link, .night-mode .nav-link,
.night-mode #mainNav.navbar-light .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link::after, .night-mode #mainNav.navbar-light .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link::after,
.night-mode #mainNav.navbar-light .navbar-collapse .navbar-sidenav .nav-link-collapse::after,
.night-mode .dropdown-item { .night-mode .dropdown-item {
color: #c1c1c1 !important; color: #c1c1c1 !important;
} }
@ -32,6 +37,11 @@
border-color: #c1c1c1; border-color: #c1c1c1;
} }
.night-mode .btn-outline-info {
color: #1ed1ee !important;
border-color: #1ed1ee !important;
}
.night-mode .btn-info { .night-mode .btn-info {
color: #c1c1c1; color: #c1c1c1;
background-color: #07373f; background-color: #07373f;
@ -46,14 +56,14 @@
.night-mode .btn-danger { .night-mode .btn-danger {
color: #c1c1c1; color: #c1c1c1;
background-color: #471116; background-color: #6f1b23;
border-color: #471116; border-color: #6f1b23;
} }
.night-mode .btn-success { .night-mode .btn-success {
color: #c1c1c1; color: #c1c1c1;
background-color: #0d3a18; background-color: #17642a;
border-color: #0d3a18; border-color: #17642a;
} }
.night-mode .btn-light, .night-mode .btn-light,
@ -71,6 +81,10 @@
border: 1px solid #ced4da; border: 1px solid #ced4da;
} }
.night-mode ::placeholder {
color: #b1bac4;
}
.night-mode .content-wrapper { .night-mode .content-wrapper {
background: #333131; background: #333131;
} }
@ -110,6 +124,11 @@
border-color: #80bdff; border-color: #80bdff;
} }
.night-mode select {
color: #ced4da;
background-color: #333131;
}
.night-mode .dropdown-item:focus, .night-mode .dropdown-item:focus,
.night-mode .dropdown-item:hover { .night-mode .dropdown-item:hover {
color: #16181b; color: #16181b;
@ -139,13 +158,13 @@
} }
.night-mode a.discrete-link:hover { .night-mode a.discrete-link:hover {
color: #16354f !important; color: #83c6ff !important;
text-decoration: none !important; text-decoration: none !important;
background-color: #333131; background-color: #333131;
} }
.night-mode a.discrete-link:focus { .night-mode a.discrete-link:focus {
color: #3a0b0f !important; color: #ffa8af !important;
background-color: #333131; background-color: #333131;
} }
@ -204,7 +223,7 @@
} }
.night-mode .active-page { .night-mode .active-page {
box-shadow: inset 5px 0 0 #350a0f !important; box-shadow: inset 5px 0 0 #ff7585 !important;
background-color: #383838 !important; background-color: #383838 !important;
} }
@ -241,3 +260,20 @@
.night-mode .dropdown-menu { .night-mode .dropdown-menu {
background-color: #333131; background-color: #333131;
} }
.night-mode .table-secondary td,
.night-mode .table-secondary th {
background-color: #4c4e50;
}
.night-mode .secondary-message {
border-top-color: #4c4e50;
}
.night-mode .normal-message {
border-top-color: #07373f;
}
.night-mode .text-muted {
color: #8f9ba5 !important;
}

View File

@ -36,7 +36,7 @@ Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
Grocy.Components.BarcodeScanner.TorchOn(track); Grocy.Components.BarcodeScanner.TorchOn(track);
} }
// Reduce the height of the video, if it's heigher than then the viewport // Reduce the height of the video, if it's higher than then the viewport
if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted) if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted)
{ {
var bc = document.getElementById('barcodescanner-container'); var bc = document.getElementById('barcodescanner-container');

View File

@ -1,7 +1,7 @@
$(".numberpicker-down-button").unbind('click').on("click", function() $(".numberpicker-down-button").unbind('click').on("click", function()
{ {
var inputElement = $(this).parent().parent().find('input[type="number"]'); var inputElement = $(this).parent().parent().find('input[type="number"]');
inputElement.val(parseFloat(inputElement.val()) - 1); inputElement.val(parseFloat(inputElement.val() || 1) - 1);
inputElement.trigger('keyup'); inputElement.trigger('keyup');
inputElement.trigger('change'); inputElement.trigger('change');
}); });
@ -9,7 +9,7 @@ $(".numberpicker-down-button").unbind('click').on("click", function()
$(".numberpicker-up-button").unbind('click').on("click", function() $(".numberpicker-up-button").unbind('click').on("click", function()
{ {
var inputElement = $(this).parent().parent().find('input[type="number"]'); var inputElement = $(this).parent().parent().find('input[type="number"]');
inputElement.val(parseFloat(inputElement.val()) + 1); inputElement.val(parseFloat(inputElement.val() || 0) + 1);
inputElement.trigger('keyup'); inputElement.trigger('keyup');
inputElement.trigger('change'); inputElement.trigger('change');
}); });

View File

@ -278,6 +278,11 @@ $("#location_id").on('change', function(e)
console.error(xhr); console.error(xhr);
} }
); );
if (document.getElementById("product_id").getAttribute("barcode") == "null")
{
ScanModeSubmit();
}
}, },
function(xhr) function(xhr)
{ {
@ -382,10 +387,9 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
ScanModeSubmit(false);
} }
} }
ScanModeSubmit(false);
}, },
function(xhr) function(xhr)
{ {
@ -393,10 +397,6 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
); );
} }
else
{
ScanModeSubmit();
}
}, },
function(xhr) function(xhr)
{ {
@ -625,6 +625,11 @@ var current_productDetails;
function RefreshForm() function RefreshForm()
{ {
var productDetails = current_productDetails; var productDetails = current_productDetails;
if (!productDetails)
{
return;
}
if (productDetails.product.enable_tare_weight_handling == 1) if (productDetails.product.enable_tare_weight_handling == 1)
{ {
$("#consume-exact-amount-group").removeClass("d-none"); $("#consume-exact-amount-group").removeClass("d-none");
@ -663,12 +668,10 @@ function ScanModeSubmit(singleUnit = true)
if (singleUnit) if (singleUnit)
{ {
$("#display_amount").val(1); $("#display_amount").val(1);
$(".input-group-productamountpicker").trigger("change");
} }
RefreshLocaleNumberInput();
$(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
if (document.getElementById("consume-form").checkValidity() === true) if (document.getElementById("consume-form").checkValidity() === true)
{ {
$('#save-consume-button').click(); $('#save-consume-button').click();

View File

@ -449,6 +449,7 @@ $(document).on("click", "#print-shopping-list-button", function(e)
callback: function() callback: function()
{ {
bootbox.hideAll(); bootbox.hideAll();
$('.modal-backdrop').remove();
$(".print-timestamp").text(moment().format("l LT")); $(".print-timestamp").text(moment().format("l LT"));
@ -470,6 +471,7 @@ $(document).on("click", "#print-shopping-list-button", function(e)
shoppingListPrintShadowTable.draw(); shoppingListPrintShadowTable.draw();
} }
$(".print-layout-container").addClass("d-none");
$("." + $("input[name='print-layout-type']:checked").val()).removeClass("d-none"); $("." + $("input[name='print-layout-type']:checked").val()).removeClass("d-none");
window.print(); window.print();

View File

@ -162,6 +162,12 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id); Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
} }
if ($("#display_amount").val().toString().isEmpty())
{
$("#display_amount").val(1);
$("#display_amount").trigger("change");
}
$('#display_amount').focus(); $('#display_amount').focus();
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
Grocy.ShoppingListItemFormInitialLoadDone = true; Grocy.ShoppingListItemFormInitialLoadDone = true;
@ -244,17 +250,14 @@ if (GetUriParam("embedded") !== undefined)
} }
var eitherRequiredFields = $("#product_id,#product_id_text_input,#note"); var eitherRequiredFields = $("#product_id,#product_id_text_input,#note");
eitherRequiredFields.on("input", function() eitherRequiredFields.prop('required', "");
eitherRequiredFields.on('input', function()
{ {
eitherRequiredFields.attr("required", ""); eitherRequiredFields.not(this).prop('required', !$(this).val().length);
if (!$(this).val().isEmpty())
{
eitherRequiredFields.not(this).removeAttr("required");
}
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
}); });
if (GetUriParam("product-name") != null) if (GetUriParam("product-name") != null)
{ {
Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.GetPicker().trigger('change');

View File

@ -229,12 +229,6 @@ function RefreshStatistics()
Grocy.Api.Get('stock', Grocy.Api.Get('stock',
function(result) function(result)
{ {
var amountSum = 0;
result.forEach(element =>
{
amountSum += parseInt(element.amount);
});
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products')); $("#info-current-stock").text(__n(result.length, '%s Product', '%s Products'));
@ -244,7 +238,7 @@ function RefreshStatistics()
var valueSum = 0; var valueSum = 0;
result.forEach(element => result.forEach(element =>
{ {
valueSum += parseInt(element.value); valueSum += parseFloat(element.value);
}); });
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products') + ", " + __t('%s total value', valueSum.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency }))); $("#info-current-stock").text(__n(result.length, '%s Product', '%s Products') + ", " + __t('%s total value', valueSum.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })));
} }

View File

@ -126,12 +126,15 @@
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{ {
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null)
{ {
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
$("#location_id_to").val(""); $("#location_id_to").val("");
$("#location_id_from").val(""); if (GetUriParam("stockId") == null)
{
$("#location_id_from").val("");
}
var productId = $(e.target).val(); var productId = $(e.target).val();
@ -185,6 +188,12 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id_from").trigger('change'); $("#location_id_from").trigger('change');
} }
}); });
if (GetUriParam("locationId") != null)
{
$("#location_id_from").val(GetUriParam("locationId"));
$("#location_id_from").trigger("change");
}
}, },
function(xhr) function(xhr)
{ {
@ -284,7 +293,7 @@ $("#location_id_from").on('change', function(e)
} }
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null)
{ {
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
} }
@ -475,6 +484,7 @@ if (GetUriParam("embedded") !== undefined)
$("#location_id_from").trigger('change'); $("#location_id_from").trigger('change');
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
$("#use_specific_stock_entry").trigger('change'); $("#use_specific_stock_entry").trigger('change');
Grocy.Components.ProductPicker.GetPicker().trigger('change');
} }
} }

View File

@ -431,11 +431,11 @@ class StockService extends BaseService
$openedDate = $stockRow->opened_date; $openedDate = $stockRow->opened_date;
if ($open && $openedDate == null) if (boolval($open) && $openedDate == null)
{ {
$openedDate = date('Y-m-d'); $openedDate = date('Y-m-d');
} }
elseif (!$open) elseif (!boolval($open))
{ {
$openedDate = null; $openedDate = null;
} }
@ -447,7 +447,7 @@ class StockService extends BaseService
'location_id' => $locationId, 'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId, 'shopping_location_id' => $shoppingLocationId,
'opened_date' => $openedDate, 'opened_date' => $openedDate,
'open' => $open, 'open' => BoolToInt($open),
'purchased_date' => $purchasedDate 'purchased_date' => $purchasedDate
]); ]);
@ -549,7 +549,6 @@ class StockService extends BaseService
{ {
$currentStock = $this->GetCurrentStock(false); $currentStock = $this->GetCurrentStock(false);
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<'); $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<');
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'due_type', 1);
if ($excludeOverdue) if ($excludeOverdue)
{ {
@ -995,8 +994,8 @@ class StockService extends BaseService
if ($productDetails->product->enable_tare_weight_handling == 1) if ($productDetails->product->enable_tare_weight_handling == 1)
{ {
// Hard fail for now, as we not yet support transfering tare weight enabled products // Hard fail for now, as we not yet support transferring tare weight enabled products
throw new \Exception('Transfering tare weight enabled products is not yet possible'); throw new \Exception('Transferring tare weight enabled products is not yet possible');
if ($amount < floatval($productDetails->product->tare_weight)) if ($amount < floatval($productDetails->product->tare_weight))
{ {
throw new \Exception('The amount cannot be lower than the defined tare weight'); throw new \Exception('The amount cannot be lower than the defined tare weight');
@ -1010,7 +1009,7 @@ class StockService extends BaseService
if ($amount > $productStockAmountAtFromLocation) if ($amount > $productStockAmountAtFromLocation)
{ {
throw new \Exception('Amount to be transfered cannot be > current stock amount at the source location'); throw new \Exception('Amount to be transferred cannot be > current stock amount at the source location');
} }
if ($specificStockEntryId !== 'default') if ($specificStockEntryId !== 'default')
@ -1141,7 +1140,7 @@ class StockService extends BaseService
'amount' => $restStockAmount 'amount' => $restStockAmount
]); ]);
// The transfered amount gets into a new stock entry // The transferred amount gets into a new stock entry
$stockEntryNew = $this->getDatabase()->stock()->createRow([ $stockEntryNew = $this->getDatabase()->stock()->createRow([
'product_id' => $stockEntry->product_id, 'product_id' => $stockEntry->product_id,
'amount' => $amount, 'amount' => $amount,

View File

@ -34,7 +34,8 @@
'min' => 0, 'min' => 0,
'isRequired' => false, 'isRequired' => false,
'additionalCssClasses' => 'userfield-input', 'additionalCssClasses' => 'userfield-input',
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"' 'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"',
'value' => ''
)) ))
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DECIMAL_NUMBER) @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DECIMAL_NUMBER)
@include('components.numberpicker', array( @include('components.numberpicker', array(
@ -45,7 +46,8 @@
'decimals' => 4, 'decimals' => 4,
'isRequired' => false, 'isRequired' => false,
'additionalCssClasses' => 'userfield-input', 'additionalCssClasses' => 'userfield-input',
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"' 'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"',
'value' => ''
)) ))
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DATE) @elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DATE)
@include('components.datetimepicker', array( @include('components.datetimepicker', array(

View File

@ -40,7 +40,7 @@
'id' => 'purchased_date', 'id' => 'purchased_date',
'label' => 'Purchased date', 'label' => 'Purchased date',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
'hint' => 'This will apply to added products', 'hint' => $__t('This will apply to added products'),
'initWithNow' => true, 'initWithNow' => true,
'limitEndToNow' => false, 'limitEndToNow' => false,
'limitStartToNow' => false, 'limitStartToNow' => false,
@ -61,7 +61,7 @@
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'label' => 'Due date', 'label' => 'Due date',
'hint' => 'This will apply to added products', 'hint' => $__t('This will apply to added products'),
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
'initWithNow' => false, 'initWithNow' => false,
'limitEndToNow' => false, 'limitEndToNow' => false,

View File

@ -688,7 +688,7 @@
@endif @endif
</td> </td>
<td class="font-italic"> <td class="font-italic">
{!! $__t('This means 1 %1$s is the same as %2$s %3$s', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->from_qu_id)->name, '<span class="locale-number locale-number-quantity-amount">' . $quConversion->factor . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name) !!} {!! $__t('This means 1 %1$s is the same as %2$s %3$s', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->from_qu_id)->name, '<span class="locale-number locale-number-quantity-amount">' . $quConversion->factor . '</span>', $__n($quConversion->factor, FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name_plural)) !!}
</td> </td>
</tr> </tr>
@endif @endif

View File

@ -311,16 +311,20 @@
<div class="row ml-1"> <div class="row ml-1">
@if(!empty($calories) && intval($calories) > 0) @if(!empty($calories) && intval($calories) > 0)
<div class="col-6 col-xl-3"> <div class="col-6 col-xl-3">
<label>{{ $__t('Calories') }}</label> <label>{{ $__t('Energy (kcal)') }}</label>&nbsp;
<i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip"
title="{{ $__t('per serving') }}"></i>
<h3 class="locale-number locale-number-generic pt-0">{{ $calories }}</h3> <h3 class="locale-number locale-number-generic pt-0">{{ $calories }}</h3>
</div> </div>
@endif @endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="col-5"> <div class="col-5">
<label>{{ $__t('Costs') }}&nbsp;</label> <label>{{ $__t('Costs') }}&nbsp;
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Based on the prices of the default consume rule which is "Opened first, then first due first, then first in first out"') }}"></i> title="{{ $__t('Based on the prices of the default consume rule which is "Opened first, then first due first, then first in first out"') }}"></i>
</label>
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3> <h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3>
</div> </div>
@endif @endif
@ -410,8 +414,10 @@
@endif @endif
{{ $__n($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} {{ $__n($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
@if(GROCY_FEATURE_FLAG_STOCK) @if(GROCY_FEATURE_FLAG_STOCK)
@if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif <span class="d-print-none">
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list', round($selectedRecipePosition->missing_amount, 2), round($selectedRecipePosition->amount_on_shopping_list, 2)) }} @endif</span> @if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list', round($selectedRecipePosition->missing_amount, 2), round($selectedRecipePosition->amount_on_shopping_list, 2)) }} @endif</span>
</span>
@endif @endif
@if($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) <span class="float-right font-italic ml-2 locale-number locale-number-currency">{{ $selectedRecipePosition->costs }}</span> @endif @if($selectedRecipePosition->need_fulfilled == 1 && GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) <span class="float-right font-italic ml-2 locale-number locale-number-currency">{{ $selectedRecipePosition->costs }}</span> @endif
<span class="float-right font-italic"><span class="locale-number locale-number-quantity-amount">{{ $selectedRecipePosition->calories }} {{ $__t('Calories') }}</span></span> <span class="float-right font-italic"><span class="locale-number locale-number-quantity-amount">{{ $selectedRecipePosition->calories }} {{ $__t('Calories') }}</span></span>

View File

@ -339,7 +339,7 @@
<span class="d-inline print-timestamp"></span> <span class="d-inline print-timestamp"></span>
</h6> </h6>
</div> </div>
<div class="w-75 print-layout-type-table d-none"> <div class="w-75 print-layout-container print-layout-type-table d-none">
<div> <div>
<table id="shopping-list-print-shadow-table" <table id="shopping-list-print-shadow-table"
class="table table-sm table-striped nowrap"> class="table table-sm table-striped nowrap">
@ -386,7 +386,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="w-75 print-layout-type-list d-none"> <div class="w-75 print-layout-container print-layout-type-list d-none">
@foreach($listItems as $listItem) @foreach($listItems as $listItem)
<div class="py-0"> <div class="py-0">
<span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural) }}@endif <span class="locale-number locale-number-quantity-amount">{{ $listItem->amount }}</span> @if(!empty($listItem->product_id)){{ $__n($listItem->amount, $listItem->qu_name, $listItem->qu_name_plural) }}@endif