mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Added a separate QU for displaying prices (closes #2225)
This commit is contained in:
parent
f4639c9bb2
commit
d03175f75a
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
128
migrations/0219.sql
Normal 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;
|
@ -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) =>
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user