Move FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT to per product option (closes #1753)

This commit is contained in:
Bernd Bestel 2022-02-07 19:12:31 +01:00
parent 12e5377c40
commit d1d52aea44
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
10 changed files with 204 additions and 25 deletions

View File

@ -1,6 +1,9 @@
- Stock entry labels get now also printed on inventory (only when adding products, same option "Stock entry label" like on the purchase page)
- Added a separate status filter and table row highlighting (blue) on the chores, tasks and batteries overview pages for items due today
- Additionally, the "due soon" days of chores/tasks/batteries (top right corner settings menu) can be set to `0` to disable that filter/highlighting
- The `config.php` option `FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT` was removed and is now a new product option `Treat opened as out of stock`, means, if opened items will be counted as missing for calculating if a product is below its minimum stock amount, can now be configured per product
- The existing option will be migrated to all existing products, so no changed behavior after the update
- There is also a new stock setting (section "Presets for new products") which can be used to configure the default when adding products (also that will be set based on the old setting on migration)
- Optimized relative time display (also fixed a phrasing problem for some languages, e.g. Hungarian) (thanks @Tallyrald)
- When using LDAP authentication, the configured `LDAP_UID_ATTR` is now used to compare if the user already exists instead of the username entered on the login page (that prevents creating multiple users if you entere the username in different notations) (thanks @FloSet)
- When using reverse proxy authentication (`ReverseProxyAuthMiddleware`), it's now also possible to pass the username in an environment variable instead of an HTTP header (new `config.php` option `REVERSE_PROXY_AUTH_USE_ENV`) (thanks @Forceu)

View File

@ -149,6 +149,7 @@ DefaultUserSetting('product_presets_location_id', -1); // Default location id fo
DefaultUserSetting('product_presets_product_group_id', -1); // Default product group id for new products (-1 means no product group is preset)
DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset)
DefaultUserSetting('product_presets_default_due_days', 0); // Default due days for new products (-1 means that the product will be never overdue)
DefaultUserSetting('product_presets_treat_opened_as_out_of_stock', true); // Default "Treat opened as out of stock" option for new products
DefaultUserSetting('stock_decimal_places_amounts', 4); // Default decimal places allowed for amounts
DefaultUserSetting('stock_decimal_places_prices', 2); // Default decimal places allowed for prices
DefaultUserSetting('stock_auto_decimal_separator_prices', false);
@ -221,5 +222,4 @@ Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true);
Setting('FEATURE_FLAG_THERMAL_PRINTER', false);
// 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_FLAG_AUTO_TORCH_ON_WITH_CAMERA', true); // Enables the torch automatically (if the device has one)

View File

@ -2286,3 +2286,9 @@ msgstr ""
msgid "Save & add another task"
msgstr ""
msgid "Treat opened as out of stock"
msgstr ""
msgid "When enabled, opened items will be counted as missing for calculating if this product is below its minimum stock amount"
msgstr ""

132
migrations/0161.sql Normal file
View File

@ -0,0 +1,132 @@
ALTER TABLE products
ADD treat_opened_as_out_of_stock TINYINT NOT NULL DEFAULT 1 CHECK(treat_opened_as_out_of_stock IN (0, 1));
DROP VIEW stock_missing_products_including_opened;
DROP VIEW stock_missing_products;
CREATE VIEW stock_missing_products
AS
-- Products WITHOUT sub products where the amount of the sub products SHOULD NOT be cumulated
SELECT
p.id,
MAX(p.name) AS name,
p.min_stock_amount - IFNULL(SUM(s.amount), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened), 0) ELSE 0 END AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products_view p
LEFT JOIN stock_current s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
AND p.has_sub_products = 0
AND p.parent_product_id IS NULL
AND IFNULL(p.active, 0) = 1
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened), 0) ELSE 0 END < p.min_stock_amount
UNION
-- Parent products WITH sub products where the amount of the sub products SHOULD be cumulated
SELECT
p.id,
MAX(p.name) AS name,
SUM(sub_p.min_stock_amount) - IFNULL(SUM(s.amount_aggregated), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened_aggregated), 0) ELSE 0 END AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products_view p
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 1
AND IFNULL(p.active, 0) = 1
GROUP BY p.id
HAVING IFNULL(SUM(s.amount_aggregated), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened_aggregated), 0) ELSE 0 END < SUM(sub_p.min_stock_amount)
UNION
-- Sub products where the amount SHOULD NOT be cumulated into the parent product
SELECT
sub_p.id,
MAX(sub_p.name) AS name,
SUM(sub_p.min_stock_amount) - IFNULL(SUM(s.amount_aggregated), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened_aggregated), 0) ELSE 0 END AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products p
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
AND IFNULL(p.active, 0) = 1
GROUP BY sub_p.id
HAVING IFNULL(SUM(s.amount), 0) - CASE WHEN p.treat_opened_as_out_of_stock = 1 THEN IFNULL(SUM(s.amount_opened), 0) ELSE 0 END < sub_p.min_stock_amount;
DROP VIEW uihelper_stock_current_overview_including_opened;
DROP VIEW uihelper_stock_current_overview;
CREATE VIEW uihelper_stock_current_overview
AS
SELECT
p.id,
sc.amount_opened AS amount_opened,
p.tare_weight AS tare_weight,
p.enable_tare_weight_handling AS enable_tare_weight_handling,
sc.amount AS amount,
sc.value as value,
sc.product_id AS product_id,
sc.best_before_date AS best_before_date,
EXISTS(SELECT id FROM stock_missing_products WHERE id = sc.product_id) AS product_missing,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name_plural,
p.name AS product_name,
(SELECT name FROM product_groups WHERE product_groups.id = p.product_group_id) AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name_plural,
sc.is_aggregated_amount,
sc.amount_opened_aggregated,
sc.amount_aggregated,
p.calories AS product_calories,
sc.amount * p.calories AS calories,
sc.amount_aggregated * p.calories AS calories_aggregated,
p.quick_consume_amount,
p.due_type,
plp.purchased_date AS last_purchased,
plp.price AS last_price,
pap.price as average_price,
p.min_stock_amount,
pbcs.barcodes AS product_barcodes,
p.description AS product_description,
l.name AS product_default_location_name,
p_parent.id AS parent_product_id,
p_parent.name AS parent_product_name,
p.picture_file_name AS product_picture_file_name
FROM (
SELECT *
FROM stock_current
WHERE best_before_date IS NOT NULL
UNION
SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type
FROM stock_missing_products m
JOIN products p
ON m.id = p.id
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products_last_purchased plp
ON sc.product_id = plp.product_id
LEFT JOIN products_average_price pap
ON sc.product_id = pap.product_id
LEFT JOIN products p
ON sc.product_id = p.id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sc.product_id = pbcs.product_id
LEFT JOIN products p_parent
ON p.parent_product_id = p_parent.id
LEFT JOIN locations l
ON p.location_id = l.id
WHERE p.hide_on_stock_overview = 0;

18
migrations/0162.php Normal file
View File

@ -0,0 +1,18 @@
<?php
// This is executed inside DatabaseMigrationService class/context
// Migrate the old config.php setting FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT
// to the new product option treat_opened_as_out_of_stock
// New and old default was/is enabled, so only disable it for all existing products when it was disabled
if (!defined('GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT'))
{
define('GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT', true);
}
if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT)
{
$this->getDatabaseService()->ExecuteDbStatement('UPDATE products SET treat_opened_as_out_of_stock = 0');
$this->getDatabaseService()->ExecuteDbStatement("INSERT INTO user_settings (user_id, key, value) SELECT id, 'product_presets_treat_opened_as_out_of_stock', '0' FROM users");
}

View File

@ -493,6 +493,11 @@ else if (Grocy.EditMode === 'create')
{
$("#default_best_before_days").val(Grocy.UserSettings.product_presets_default_due_days);
}
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
{
$("#treat_opened_as_out_of_stock").prop("checked", BoolVal(Grocy.UserSettings.product_presets_treat_opened_as_out_of_stock));
}
}
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)

View File

@ -2,6 +2,10 @@
$("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
$("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id);
$("#product_presets_default_due_days").val(Grocy.UserSettings.product_presets_default_due_days);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING && BoolVal(Grocy.UserSettings.product_presets_treat_opened_as_out_of_stock))
{
$("#product_presets_treat_opened_as_out_of_stock").prop("checked", true);
}
$("#stock_due_soon_days").val(Grocy.UserSettings.stock_due_soon_days);
$("#stock_default_purchase_amount").val(Grocy.UserSettings.stock_default_purchase_amount);
$("#stock_default_consume_amount").val(Grocy.UserSettings.stock_default_consume_amount);

View File

@ -604,14 +604,7 @@ class StockService extends BaseService
$sql = 'SELECT * FROM stock_current';
if ($includeNotInStockButMissingProducts)
{
$missingProductsView = 'stock_missing_products_including_opened';
if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT)
{
$missingProductsView = 'stock_missing_products';
}
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)';
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, 0, 0, null, 0, 0, 0 FROM stock_missing_products WHERE id NOT IN (SELECT product_id FROM stock_current)';
}
$currentStockMapped = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_GROUP | \PDO::FETCH_OBJ);
@ -639,16 +632,9 @@ class StockService extends BaseService
}
public function GetCurrentStockOverview()
{
if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT)
{
return $this->getDatabase()->uihelper_stock_current_overview();
}
else
{
return $this->getDatabase()->uihelper_stock_current_overview_including_opened();
}
}
public function GetDueProducts(int $days = 5, bool $excludeOverdue = false)
{
@ -674,13 +660,7 @@ class StockService extends BaseService
public function GetMissingProducts()
{
$sql = 'SELECT * FROM stock_missing_products_including_opened';
if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT)
{
$sql = 'SELECT * FROM stock_missing_products';
}
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->getDatabaseService()->ExecuteDbQuery('SELECT * FROM stock_missing_products')->fetchAll(\PDO::FETCH_OBJ);
}
public function GetProductDetails(int $productId)

View File

@ -160,7 +160,7 @@
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount'
))
<div class="form-group">
<div class="form-group @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) mb-1 @endif">
<div class="custom-control custom-checkbox">
<input @if($mode=='edit'
&&
@ -175,6 +175,22 @@
</div>
</div>
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<div class="form-group">
<div class="custom-control custom-checkbox">
<input @if($mode=='edit'
&&
$product->treat_opened_as_out_of_stock == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="treat_opened_as_out_of_stock" name="treat_opened_as_out_of_stock" value="1">
<label class="form-check-label custom-control-label"
for="treat_opened_as_out_of_stock">{{ $__t('Treat opened as out of stock') }}&nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('When enabled, opened items will be counted as missing for calculating if this product is below its minimum stock amount') }}"></i>
</label>
</div>
</div>
@endif
@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<div class="form-group">
<label class="d-block my-0"

View File

@ -63,6 +63,21 @@
'min' => -1,
'additionalCssClasses' => 'user-setting-control'
))
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="form-check-input custom-control-input user-setting-control"
id="product_presets_treat_opened_as_out_of_stock"
data-setting-key="product_presets_treat_opened_as_out_of_stock">
<label class="form-check-label custom-control-label"
for="product_presets_treat_opened_as_out_of_stock">
{{ $__t('Treat opened as out of stock') }}
</label>
</div>
</div>
@endif
</div>
<h4 class="mt-2">{{ $__t('Stock overview') }}</h4>