Implemented "Default quantity unit consume" (closes #1845)

This commit is contained in:
Bernd Bestel
2022-12-26 11:11:55 +01:00
parent 39d1f49431
commit 0585e80c70
10 changed files with 204 additions and 25 deletions
+7 -1
View File
@@ -14,6 +14,10 @@
- The product option "Factor purchase to stock quantity unit" was removed
- => Use normal product specific QU conversions instead, if needed
- An existing "Factor purchase to stock quantity unit" was automatically migrated to a product specific QU conversion
- New product option "Default quantity unit consume"
- Will be used/selected as the default quantity unit on the consume page
- The product's "Quick consume amount" is now displayed related to this quantity unit ("quick consume/open buttons" on the stock overview page)
- Defaults to the product's "Quantity unit stock" (so no changed behavior when not configured)
- Fixed that hiding the "Purchased date" column (table options) on the stock entries page didn't work
- Fixed that the consumed amount was wrong, when consuming multiple substituted subproducts at once and when multiple/different conversion factors were involved
@@ -63,8 +67,10 @@
- ⚠️ **Breaking changes**:
- The product property `qu_factor_purchase_to_stock` was removed (existing factors were migrated to normal product specific QU conversions, see above)
- The endpoint `/stock/products/{productId}` returns a new field/property `qu_conversion_factor_purchase_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock)
- Numbers are now returned as numbers (so technically without quotes around them, were strings for nearly all endpoints before)
- Endpoint `/stock/products/{productId}`:
- Added a new field/property `qu_conversion_factor_purchase_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock)
- Added a new field/property `default_quantity_unit_consume` (contains the quantity unit object of the product's "Default quantity unit consume")
- The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit)
- `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units)
- The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`)
+2 -2
View File
@@ -163,7 +163,7 @@ class StockController extends BaseController
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'referencedQuantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'),
@@ -182,7 +182,7 @@ class StockController extends BaseController
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'),
+4 -1
View File
@@ -4732,10 +4732,13 @@
"product_barcodes": {
"$ref": "#/components/schemas/ProductBarcode"
},
"quantity_unit_stock": {
"$ref": "#/components/schemas/QuantityUnit"
},
"default_quantity_unit_purchase": {
"$ref": "#/components/schemas/QuantityUnit"
},
"quantity_unit_stock": {
"default_quantity_unit_consume": {
"$ref": "#/components/schemas/QuantityUnit"
},
"last_purchased": {
+6
View File
@@ -2344,3 +2344,9 @@ msgstr ""
msgid "Product specifc QU conversions"
msgstr ""
msgid "Default quantity unit consume"
msgstr ""
msgid "This is the default quantity unit used when consuming this product"
msgstr ""
+135
View File
@@ -0,0 +1,135 @@
ALTER TABLE products
ADD qu_id_consume INTEGER;
UPDATE products
SET qu_id_consume = qu_id_stock;
CREATE TRIGGER default_qu_id_consume AFTER INSERT ON products
BEGIN
UPDATE products
SET qu_id_consume = qu_id_stock
WHERE id = NEW.id
AND IFNULL(qu_id_consume, 0) = 0;
END;
DROP TRIGGER default_qu_conversion;
CREATE TRIGGER default_qu_conversions AFTER INSERT ON products
BEGIN
/*
Automatically create a default (product specific 1 to 1) QU conversion when a product
with qu_stock != qu_purchase was created and when no default QU conversion applies
*/
INSERT INTO quantity_unit_conversions
(from_qu_id, to_qu_id, factor, product_id)
SELECT p.qu_id_purchase, p.qu_id_stock, 1, p.id
FROM products p
WHERE p.id = NEW.id
AND p.qu_id_stock != qu_id_purchase
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions_resolved WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_purchase);
/*
Automatically create a default (product specific 1 to 1) QU conversion when a product
with qu_stock != qu_consume was created and when no default QU conversion applies
*/
INSERT INTO quantity_unit_conversions
(from_qu_id, to_qu_id, factor, product_id)
SELECT p.qu_id_consume, p.qu_id_stock, 1, p.id
FROM products p
WHERE p.id = NEW.id
AND p.qu_id_stock != qu_id_consume
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions_resolved WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_consume);
END;
DROP VIEW products_view;
CREATE VIEW products_view
AS
SELECT
p.*,
CASE WHEN (SELECT 1 FROM products WHERE parent_product_id = p.id) NOTNULL THEN 1 ELSE 0 END AS has_sub_products,
IFNULL(quc_purchase.factor, 1.0) AS qu_factor_purchase_to_stock,
IFNULL(quc_consume.factor, 1.0) AS qu_factor_consume_to_stock
FROM products p
LEFT JOIN quantity_unit_conversions_resolved quc_purchase
ON p.id = quc_purchase.product_id
AND p.qu_id_purchase = quc_purchase.from_qu_id
AND p.qu_id_stock = quc_purchase.to_qu_id
LEFT JOIN quantity_unit_conversions_resolved quc_consume
ON p.id = quc_consume.product_id
AND p.qu_id_consume = quc_consume.from_qu_id
AND p.qu_id_stock = quc_consume.to_qu_id;
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,
p.name AS product_name,
pg.name AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
qu_stock.name AS qu_stock_name,
qu_stock.name_plural AS qu_stock_name_plural,
qu_purchase.name AS qu_purchase_name,
qu_purchase.name_plural AS qu_purchase_name_plural,
qu_consume.name AS qu_consume_name,
qu_consume.name_plural AS qu_consume_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.quick_consume_amount / p.qu_factor_consume_to_stock AS quick_consume_amount_qu_consume,
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,
p.no_own_stock AS product_no_own_stock,
p.qu_factor_purchase_to_stock AS product_qu_factor_purchase_to_stock
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
JOIN products_view p
ON sc.product_id = p.id
JOIN locations l
ON p.location_id = l.id
JOIN quantity_units qu_stock
ON p.qu_id_stock = qu_stock.id
JOIN quantity_units qu_purchase
ON p.qu_id_purchase = qu_purchase.id
JOIN quantity_units qu_consume
ON p.qu_id_consume = qu_consume.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
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 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
WHERE p.hide_on_stock_overview = 0;
+4 -4
View File
@@ -106,7 +106,7 @@
$("#display_amount").removeAttr("max");
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount))
{
$('#display_amount').val(productDetails.product.quick_consume_amount);
$('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor"));
}
else
{
@@ -196,7 +196,7 @@ $('#save-mark-as-open-button').on('click', function(e)
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount))
{
$('#display_amount').val(productDetails.product.quick_consume_amount);
$('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor"));
}
else
{
@@ -365,10 +365,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
current_productDetails = productDetails;
Grocy.Components.ProductAmountPicker.Reload(productDetails.product.id, productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.quantity_unit_stock.id);
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_consume.id);
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount))
{
$('#display_amount').val(productDetails.product.quick_consume_amount);
$('#display_amount').val(productDetails.product.quick_consume_amount * $("#qu_id option:selected").attr("data-qu-factor"));
}
else
{
+9 -2
View File
@@ -358,15 +358,22 @@ $(document).on('click', '.barcode-delete-button', function(e)
$('#qu_id_stock').change(function(e)
{
// Preset QU purchase with stock QU if unset
// Preset qu_id_purchase and qu_id_consume by qu_id_stock if unset
var quIdStock = $('#qu_id_stock');
var quIdPurchase = $('#qu_id_purchase');
var quIdConsume = $('#qu_id_consume');
if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0)
{
quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex;
Grocy.FrontendHelpers.ValidateForm('product-form');
}
if (quIdConsume[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0)
{
quIdConsume[0].selectedIndex = quIdStock[0].selectedIndex;
}
Grocy.FrontendHelpers.ValidateForm('product-form');
});
$(window).on("message", function(e)
+3 -1
View File
@@ -747,6 +747,7 @@ class StockService extends BaseService
$nextDueDate = $this->getDatabase()->stock()->where('product_id', $productId)->min('best_before_date');
$quPurchase = $this->getDatabase()->quantity_units($product->qu_id_purchase);
$quStock = $this->getDatabase()->quantity_units($product->qu_id_stock);
$quConsume = $this->getDatabase()->quantity_units($product->qu_id_consume);
$location = $this->getDatabase()->locations($product->location_id);
$averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days);
$currentPrice = $this->getDatabase()->products_current_price()->where('product_id', $productId)->fetch()->price;
@@ -785,8 +786,9 @@ class StockService extends BaseService
'stock_amount_opened' => $stockCurrentRow->amount_opened,
'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated,
'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated,
'default_quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'default_quantity_unit_purchase' => $quPurchase,
'default_quantity_unit_consume' => $quConsume,
'last_price' => $lastPrice,
'avg_price' => $avgPrice,
'oldest_price' => $currentPrice, // Deprecated
+21 -1
View File
@@ -395,7 +395,7 @@
id="qu_id_purchase"
name="qu_id_purchase">
<option></option>
@foreach($quantityunitsPurchase as $quantityunit)
@foreach($referencedQuantityunits as $quantityunit)
<option @if($mode=='edit'
&&
$quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@@ -403,6 +403,26 @@
</select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div>
<div class="form-group">
<label for="qu_id_consume">{{ $__t('Default quantity unit consume') }}</label>
<i class="fa-solid fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('This is the default quantity unit used when consuming this product') }}"></i>
<select required
class="custom-control custom-select input-group-qu"
id="qu_id_consume"
name="qu_id_consume">
<option></option>
@foreach($referencedQuantityunits as $quantityunit)
<option @if($mode=='edit'
&&
$quantityunit->id == $product->qu_id_consume) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div>
<div class="form-group mb-1">
<div class="custom-control custom-checkbox">
+13 -13
View File
@@ -192,12 +192,12 @@
href="#"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Consume %1$s of %2$s', floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}"
title="{{ $__t('Consume %1$s of %2$s', floatval($currentStockEntry->quick_consume_amount_qu_consume) . ' ' . $currentStockEntry->qu_consume_name, $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_stock_name }}"
data-consume-amount="{{ $currentStockEntry->quick_consume_amount }}">
<i class="fa-solid fa-utensils"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span>
<i class="fa-solid fa-utensils"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount_qu_consume }}</span>
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button"
class="permission-STOCK_CONSUME btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount_aggregated == 0) disabled @endif"
@@ -207,7 +207,7 @@
title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_stock_name }}"
data-consume-amount="@if($currentStockEntry->enable_tare_weight_handling == 1){{$currentStockEntry->tare_weight}}@else{{$currentStockEntry->amount}}@endif"
data-original-total-stock-amount="{{$currentStockEntry->amount}}">
<i class="fa-solid fa-utensils"></i> {{ $__t('All') }}
@@ -217,12 +217,12 @@
href="#"
data-toggle="tooltip"
data-placement="left"
title="{{ $__t('Mark %1$s of %2$s as open', floatval($currentStockEntry->quick_consume_amount) . ' ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}"
title="{{ $__t('Mark %1$s of %2$s as open', floatval($currentStockEntry->quick_consume_amount_qu_consume) . ' ' . $currentStockEntry->qu_consume_name, $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_stock_name }}"
data-open-amount="{{ $currentStockEntry->quick_consume_amount }}">
<i class="fa-solid fa-box-open"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount }}</span>
<i class="fa-solid fa-box-open"></i> <span class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->quick_consume_amount_qu_consume }}</span>
</a>
@endif
<div class="dropdown d-inline-block">
@@ -326,14 +326,14 @@
<span class="custom-sort d-none">@if($currentStockEntry->product_no_own_stock == 1){{ $currentStockEntry->amount_aggregated }}@else{{ $currentStockEntry->amount }}@endif</span>
<span class="@if($currentStockEntry->product_no_own_stock == 1) d-none @endif">
<span id="product-{{ $currentStockEntry->product_id }}-amount"
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_stock_name, $currentStockEntry->qu_stock_name_plural) }}</span>
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount"
class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
</span>
@if($currentStockEntry->is_aggregated_amount == 1)
<span class="@if($currentStockEntry->product_no_own_stock == 0) pl-1 @endif text-secondary">
<i class="fa-solid fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated"
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural, true) }}
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, $currentStockEntry->qu_stock_name, $currentStockEntry->qu_stock_name_plural, true) }}
@if($currentStockEntry->amount_opened_aggregated > 0)
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated"
class="small font-italic">
@@ -407,8 +407,8 @@
<span data-toggle="tooltip"
data-trigger="hover click"
data-html="true"
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $currentStockEntry->last_price . '</span>', $currentStockEntry->qu_unit_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($currentStockEntry->last_price) * floatval($currentStockEntry->product_qu_factor_purchase_to_stock) . '</span>', $currentStockEntry->qu_purchase_unit_name) !!}
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $currentStockEntry->last_price . '</span>', $currentStockEntry->qu_stock_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($currentStockEntry->last_price) * floatval($currentStockEntry->product_qu_factor_purchase_to_stock) . '</span>', $currentStockEntry->qu_purchase_name) !!}
</span>
@endif
</td>
@@ -437,8 +437,8 @@
<span data-toggle="tooltip"
data-trigger="hover click"
data-html="true"
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $currentStockEntry->average_price . '</span>', $currentStockEntry->qu_unit_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($currentStockEntry->average_price) * floatval($currentStockEntry->product_qu_factor_purchase_to_stock) . '</span>', $currentStockEntry->qu_purchase_unit_name) !!}
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $currentStockEntry->average_price . '</span>', $currentStockEntry->qu_stock_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($currentStockEntry->average_price) * floatval($currentStockEntry->product_qu_factor_purchase_to_stock) . '</span>', $currentStockEntry->qu_purchase_name) !!}
</span>
@endif
</td>