Added a separate QU for displaying prices (closes #2225)

This commit is contained in:
Bernd Bestel 2023-05-18 13:37:13 +02:00
parent f4639c9bb2
commit d03175f75a
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
10 changed files with 203 additions and 11 deletions

View File

@ -20,6 +20,9 @@
- 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)
- New product option "Quantity unit for prices"
- Prices are now shown related to this quantity unit (instead of per "Default quantity unit purchase") on the product card, price history chart, stock overiew and stock entries page
- Defaults to the product's "Default quantity unit purchase" (so no changed behavior when not configured)
- Changed that when the ingredient option "Only check if any amount is in stock" is enabled, costs and calories are now based on the original entered amount instead of an "virtual" fixed amount of `1`
- When using the "Add as barcode to existing product" workflow on a purchase transaction, the selected quantity unit and the entered amount is now also added to the new barcode
- When using the "Add as barcode to existing product" workflow on a purchase or inventory transaction, the entered note is now also added to the new barcode
@ -103,6 +106,7 @@
- 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 `qu_conversion_factor_price_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_price 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)

View File

@ -4744,6 +4744,9 @@
"default_quantity_unit_consume": {
"$ref": "#/components/schemas/QuantityUnit"
},
"quantity_unit_price": {
"$ref": "#/components/schemas/QuantityUnit"
},
"last_purchased": {
"type": "string",
"format": "date"
@ -4801,6 +4804,10 @@
"qu_conversion_factor_purchase_to_stock": {
"type": "number",
"description": "The conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock"
},
"qu_conversion_factor_price_to_stock": {
"type": "number",
"description": "The conversion factor of the corresponding QU conversion from the product's qu_id_price to qu_id_stock"
}
},
"example": {
@ -4852,6 +4859,14 @@
"name_plural": "Packs",
"plural_forms": null
},
"quantity_unit_price": {
"id": "3",
"name": "Pack",
"description": null,
"row_created_timestamp": "2019-05-02 20:12:25",
"name_plural": "Packs",
"plural_forms": null
},
"last_price": null,
"avg_price": null,
"current_price": null,

View File

@ -1778,7 +1778,7 @@ msgstr ""
msgid "Show a QR-Code for this API key"
msgstr ""
msgid "This is the default quantity unit used when adding this product to the shopping list"
msgid "This is the default quantity unit used on purchase and when adding this product to the shopping list"
msgstr ""
msgid "Show a warning when the due date of the purchased product is earlier than the next due date in stock"
@ -2415,3 +2415,9 @@ msgstr ""
msgid "Out-of-stock products"
msgstr ""
msgid "Quantity unit for prices"
msgstr ""
msgid "When displaying prices for this product, they will be related to this quantity unit"
msgstr ""

128
migrations/0219.sql Normal file
View File

@ -0,0 +1,128 @@
ALTER TABLE products
ADD qu_id_price INTEGER;
UPDATE products
SET qu_id_price = qu_id_purchase;
CREATE TRIGGER default_qu_id_price AFTER INSERT ON products
BEGIN
UPDATE products
SET qu_id_price = qu_id_purchase
WHERE id = NEW.id
AND IFNULL(qu_id_price, 0) = 0;
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,
IFNULL(quc_price.factor, 1.0) AS qu_factor_price_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
LEFT JOIN quantity_unit_conversions_resolved quc_price
ON p.id = quc_price.product_id
AND p.qu_id_price = quc_price.from_qu_id
AND p.qu_id_stock = quc_price.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,
qu_price.name AS qu_price_name,
qu_price.name_plural AS qu_price_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.quick_open_amount,
p.quick_open_amount / p.qu_factor_consume_to_stock AS quick_open_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,
p.qu_factor_price_to_stock AS product_qu_factor_price_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
JOIN quantity_units qu_price
ON p.qu_id_price = qu_price.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;
DROP VIEW uihelper_stock_entries;
CREATE VIEW uihelper_stock_entries
AS
SELECT
*
FROM stock s
JOIN products_view p
ON s.product_id = p.id;

View File

@ -84,7 +84,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null)
{
$('#productcard-product-last-price').text(__t("%1$s per %2$s", (productDetails.last_price * productDetails.qu_conversion_factor_purchase_to_stock).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name));
$('#productcard-product-last-price').text(__t("%1$s per %2$s", (productDetails.last_price * productDetails.qu_conversion_factor_price_to_stock).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_price.name));
$('#productcard-product-last-price').attr("data-original-title", __t("%1$s per %2$s", productDetails.last_price.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name));
}
else
@ -95,7 +95,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.avg_price !== null)
{
$('#productcard-product-average-price').text(__t("%1$s per %2$s", (productDetails.avg_price * productDetails.qu_conversion_factor_purchase_to_stock).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name));
$('#productcard-product-average-price').text(__t("%1$s per %2$s", (productDetails.avg_price * productDetails.qu_conversion_factor_price_to_stock).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_price.name));
$('#productcard-product-average-price').attr("data-original-title", __t("%1$s per %2$s", productDetails.avg_price.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name));
}
else
@ -151,7 +151,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
datasets[key] = []
}
chart.labels.push(moment(dataPoint.date).toDate());
datasets[key].push({ x: moment(dataPoint.date).toDate(), y: dataPoint.price * productDetails.qu_conversion_factor_purchase_to_stock });
datasets[key].push({ x: moment(dataPoint.date).toDate(), y: dataPoint.price * productDetails.qu_conversion_factor_price_to_stock });
});
Object.keys(datasets).forEach((key) =>

View File

@ -354,10 +354,11 @@ $(document).on('click', '.barcode-delete-button', function(e)
$('#qu_id_stock').change(function(e)
{
// Preset qu_id_purchase and qu_id_consume by qu_id_stock if unset
// Preset qu_id_purchase/qu_id_consume/qu_id_price by qu_id_stock if unset
var quIdStock = $('#qu_id_stock');
var quIdPurchase = $('#qu_id_purchase');
var quIdConsume = $('#qu_id_consume');
var quIdPrice = $('#qu_id_price');
if (quIdPurchase[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0)
{
@ -369,6 +370,11 @@ $('#qu_id_stock').change(function(e)
quIdConsume[0].selectedIndex = quIdStock[0].selectedIndex;
}
if (quIdPrice[0].selectedIndex === 0 && quIdStock[0].selectedIndex !== 0)
{
quIdPrice[0].selectedIndex = quIdStock[0].selectedIndex;
}
Grocy.FrontendHelpers.ValidateForm('product-form');
});

View File

@ -748,6 +748,7 @@ class StockService extends BaseService
$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);
$quPrice = $this->getDatabase()->quantity_units($product->qu_id_price);
$location = $this->getDatabase()->locations($product->location_id);
$averageShelfLifeDays = $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;
@ -776,6 +777,16 @@ class StockService extends BaseService
}
}
$quConversionFactorPriceToStock = 1.0;
if ($product->qu_id_stock != $product->qu_id_price)
{
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_price, $product->qu_id_stock)->fetch();
if ($conversion != null)
{
$quConversionFactorPriceToStock = $conversion->factor;
}
}
return [
'product' => $product,
'product_barcodes' => $productBarcodes,
@ -789,6 +800,7 @@ class StockService extends BaseService
'quantity_unit_stock' => $quStock,
'default_quantity_unit_purchase' => $quPurchase,
'default_quantity_unit_consume' => $quConsume,
'quantity_unit_price' => $quPrice,
'last_price' => $lastPrice,
'avg_price' => $avgPrice,
'oldest_price' => $currentPrice, // Deprecated
@ -802,7 +814,8 @@ class StockService extends BaseService
'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount,
'has_childs' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
'default_consume_location' => $defaultConsumeLocation,
'qu_conversion_factor_purchase_to_stock' => $quConversionFactorPurchaseToStock
'qu_conversion_factor_purchase_to_stock' => $quConversionFactorPurchaseToStock,
'qu_conversion_factor_price_to_stock' => $quConversionFactorPriceToStock
];
}

View File

@ -389,7 +389,7 @@
<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 adding this product to the shopping list') }}"></i>
title="{{ $__t('This is the default quantity unit used on purchase and when adding this product to the shopping list') }}"></i>
<select required
class="custom-control custom-select input-group-qu"
id="qu_id_purchase"
@ -424,6 +424,26 @@
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div>
<div class="form-group">
<label for="qu_id_price">{{ $__t('Quantity unit for prices') }}</label>
<i class="fa-solid fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('When displaying prices for this product, they will be related to this quantity unit') }}"></i>
<select required
class="custom-control custom-select input-group-qu"
id="qu_id_price"
name="qu_id_price">
<option></option>
@foreach($referencedQuantityunits as $quantityunit)
<option @if($mode=='edit'
&&
$quantityunit->id == $product->qu_id_price) 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">
<input @if($mode=='edit'
@ -499,7 +519,7 @@
'contextInfoId' => 'quick_consume_qu_info',
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount'
))
@php if($mode == 'edit') { $value = $product->quick_open_amount; } else { $value = 1; } @endphp
@include('components.numberpicker', array(
'id' => 'quick_open_amount',

View File

@ -287,7 +287,7 @@
data-trigger="hover click"
data-html="true"
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $stockEntry->price . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $stockEntry->price * $stockEntry->product_qu_factor_purchase_to_stock . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_purchase)->name) !!}
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $stockEntry->price * $stockEntry->qu_factor_price_to_stock . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_price)->name) !!}
</span>
</td>
<td>

View File

@ -422,7 +422,7 @@
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_stock_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $currentStockEntry->last_price * $currentStockEntry->product_qu_factor_purchase_to_stock . '</span>', $currentStockEntry->qu_purchase_name) !!}
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $currentStockEntry->last_price * $currentStockEntry->product_qu_factor_price_to_stock . '</span>', $currentStockEntry->qu_price_name) !!}
</span>
@endif
</td>
@ -452,7 +452,7 @@
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_stock_name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $currentStockEntry->average_price * $currentStockEntry->product_qu_factor_purchase_to_stock . '</span>', $currentStockEntry->qu_purchase_name) !!}
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . $currentStockEntry->average_price * $currentStockEntry->product_qu_factor_price_to_stock . '</span>', $currentStockEntry->qu_price_name) !!}
</span>
@endif
</td>