Added a new product option "Disable own stock" (closes #564)

This commit is contained in:
Bernd Bestel 2022-04-01 18:49:17 +02:00
parent b53d1a076f
commit ccc59dfc8b
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
10 changed files with 132 additions and 19 deletions

View File

@ -28,6 +28,8 @@
- It's now possible to change a products stock QU, even after it was once added to stock - It's now possible to change a products stock QU, even after it was once added to stock
- When the product was once added to stock, there needs to exist a corresponding unit conversion for the new QU - When the product was once added to stock, there needs to exist a corresponding unit conversion for the new QU
- New product option "Disable own stock" (defaults to disabled)
- When enabled, the corresponding product can't have own stock, means it will not be selectable on purchase (useful for parent products which are just used as a summary/total view of the child products)
- Added the product grocycode as a (hidden by default) column to the products list (master data) - Added the product grocycode as a (hidden by default) column to the products list (master data)
- Fixed that consuming via the consume page was not possible when `FEATURE_FLAG_STOCK_LOCATION_TRACKING` was disabled - Fixed that consuming via the consume page was not possible when `FEATURE_FLAG_STOCK_LOCATION_TRACKING` was disabled
@ -91,5 +93,5 @@
- Added a new endpoint `GET /stock/locations/{locationId}/entries` to get all stock entries of a given location (similar to the already existing endpoint `GET /stock/products/{productId}/entries`) - Added a new endpoint `GET /stock/locations/{locationId}/entries` to get all stock entries of a given location (similar to the already existing endpoint `GET /stock/products/{productId}/entries`)
- Endpoint `/recipes/{recipeId}/consume`: Fixed that consuming partially fulfilled recipes was possible, although an error was already returned in that case (and potentially some of the in-stock ingredients were consumed in fact) - Endpoint `/recipes/{recipeId}/consume`: Fixed that consuming partially fulfilled recipes was possible, although an error was already returned in that case (and potentially some of the in-stock ingredients were consumed in fact)
- Endpoint `/stock/products/{productId}`: - Endpoint `/stock/products/{productId}`:
- New field/property `current_price` which returns the current price of the corresponding products based on the stock entry to use next defined by the default consume rule (Opened first, then first due first, then first in first out) - New field/property `current_price` which returns the current price of the corresponding product, based on the stock entry to use next (defined by the default consume rule "Opened first, then first due first, then first in first out") or on the last price if the product is currently not in stock
- The field/property `oldest_price` is deprecated and will be removed in a future version (this had no real sense, currently returns the same as `current_price`) - The field/property `oldest_price` is deprecated and will be removed in a future version (this had no real sense, currently returns the same as `current_price`)

View File

@ -24,7 +24,7 @@ class StockController extends BaseController
public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'inventory', [ return $this->renderPage($response, 'inventory', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1 AND no_own_stock = 0')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(), 'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
@ -259,7 +259,7 @@ class StockController extends BaseController
public function Purchase(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Purchase(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'purchase', [ return $this->renderPage($response, 'purchase', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1 AND no_own_stock = 0')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(), 'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
@ -512,7 +512,7 @@ class StockController extends BaseController
public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
return $this->renderPage($response, 'transfer', [ return $this->renderPage($response, 'transfer', [
'products' => $this->getDatabase()->products()->where('active = 1')->where('id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)')->orderBy('name', 'COLLATE NOCASE'), 'products' => $this->getDatabase()->products()->where('active = 1')->where('no_own_stock = 0 AND id IN (SELECT product_id from stock_current WHERE amount_aggregated > 0)')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(), 'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),

View File

@ -4475,6 +4475,9 @@
"treat_opened_as_out_of_stock": { "treat_opened_as_out_of_stock": {
"type": "integer" "type": "integer"
}, },
"no_own_stock": {
"type": "integer"
},
"userfields": { "userfields": {
"type": "object", "type": "object",
"description": "Key/value pairs of userfields" "description": "Key/value pairs of userfields"
@ -4756,11 +4759,11 @@
}, },
"avg_price": { "avg_price": {
"type": "number", "type": "number",
"description": "The average price af all items currently in stock of the corresponding product" "description": "The average price af all stock entries currently in stock of the corresponding product"
}, },
"current_price": { "current_price": {
"type": "number", "type": "number",
"description": "The current price of the corresponding products, based on the stock entry to use next defined by the default consume rule (Opened first, then first due first, then first in first out)" "description": "The current price of the corresponding product, based on the stock entry to use next (defined by the default consume rule \"Opened first, then first due first, then first in first out\") or on the last price if the product is currently not in stock"
}, },
"oldest_price": { "oldest_price": {
"type": "number", "type": "number",
@ -4781,7 +4784,7 @@
}, },
"has_childs": { "has_childs": {
"type": "boolean", "type": "boolean",
"description": "True when the product is a parent products of others" "description": "True when the product is a parent product of others"
} }
}, },
"example": { "example": {

View File

@ -2320,3 +2320,9 @@ msgstr ""
msgid "The higher this number is, the more ingredients currently in stock are due soon, overdue or already expired" msgid "The higher this number is, the more ingredients currently in stock are due soon, overdue or already expired"
msgstr "" msgstr ""
msgid "Disable own stock"
msgstr ""
msgid "When enabled, this product can't have own stock, means it will not be selectable on purchase (useful for parent products which are just used as a summary/total view of the child products)"
msgstr ""

66
migrations/0180.sql Normal file
View File

@ -0,0 +1,66 @@
ALTER TABLE products
ADD no_own_stock TINYINT NOT NULL DEFAULT 0 CHECK(no_own_stock IN (0, 1));
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,
p.no_own_stock AS product_no_own_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
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;

View File

@ -110,7 +110,16 @@ Grocy.Components.ProductCard.Refresh = function(productId)
$("#productcard-product-picture").addClass("d-none"); $("#productcard-product-picture").addClass("d-none");
} }
$("#productcard-product-stock-amount-wrapper").removeClass("d-none");
$("#productcard-aggregated-amounts").addClass("pl-2");
if (productDetails.product.no_own_stock == 1)
{
$("#productcard-product-stock-amount-wrapper").addClass("d-none");
$("#productcard-aggregated-amounts").removeClass("pl-2");
}
RefreshContextualTimeago(".productcard"); RefreshContextualTimeago(".productcard");
RefreshLocaleNumberDisplay(".productcard");
}, },
function(xhr) function(xhr)
{ {

View File

@ -122,11 +122,19 @@ if (!prefillProductId2.isEmpty())
if (typeof prefillProductId !== "undefined") if (typeof prefillProductId !== "undefined")
{ {
$('#product_id').val(prefillProductId); $('#product_id').val(prefillProductId);
if ($('#product_id').val() != null)
{
$('#product_id').data('combobox').refresh(); $('#product_id').data('combobox').refresh();
$('#product_id').trigger('change'); $('#product_id').trigger('change');
var nextInputElement = $(Grocy.Components.ProductPicker.GetPicker().parent().data('next-input-selector').toString()); var nextInputElement = $(Grocy.Components.ProductPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus(); nextInputElement.focus();
}
else
{
Grocy.Components.ProductPicker.GetInputElement().focus();
}
} }
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct")

View File

@ -38,8 +38,11 @@
href="#productcard-product-description">{{ $__t('Show more') }}</a> href="#productcard-product-description">{{ $__t('Show more') }}</a>
</div> </div>
<strong>{{ $__t('Stock amount') }}:</strong> <span id="productcard-product-stock-amount" <strong>{{ $__t('Stock amount') }}:</strong>
<span id="productcard-product-stock-amount-wrapper">
<span id="productcard-product-stock-amount"
class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name"></span> class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name"></span>
</span>
<span id="productcard-product-stock-opened-amount" <span id="productcard-product-stock-opened-amount"
class="small font-italic locale-number locale-number-quantity-amount"></span> class="small font-italic locale-number locale-number-quantity-amount"></span>
<span id="productcard-aggregated-amounts" <span id="productcard-aggregated-amounts"

View File

@ -495,6 +495,20 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input @if($mode=='edit'
&&
$product->no_own_stock == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="no_own_stock" name="no_own_stock" value="1">
<label class="form-check-label custom-control-label"
for="no_own_stock">{{ $__t('Disable own stock') }}&nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('When enabled, this product can\'t have own stock, means it will not be selectable on purchase (useful for parent products which are just used as a summary/total view of the child products)') }}"></i>
</label>
</div>
</div>
<div class="sticky-form-footer pt-1"> <div class="sticky-form-footer pt-1">
<small id="save-hint" <small id="save-hint"
class="my-1 form-text text-muted @if($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add quantity unit conversions & barcodes') }}</small> class="my-1 form-text text-muted @if($mode == 'edit') d-none @endif">{{ $__t('Save & continue to add quantity unit conversions & barcodes') }}</small>

View File

@ -321,13 +321,15 @@
<td> <td>
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif @if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif
</td> </td>
<td data-order="{{ $currentStockEntry->amount }}"> <td data-order="@if($currentStockEntry->product_no_own_stock == 1){{ $currentStockEntry->amount_aggregated }}@else{{ $currentStockEntry->amount }}@endif">
<span class="@if($currentStockEntry->product_no_own_stock == 1) d-none @endif">
<span id="product-{{ $currentStockEntry->product_id }}-amount" <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_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount" <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> class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
</span>
@if($currentStockEntry->is_aggregated_amount == 1) @if($currentStockEntry->is_aggregated_amount == 1)
<span class="pl-1 text-secondary"> <span class="@if($currentStockEntry->product_no_own_stock == 0) pl-1 @endif text-secondary">
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" <i class="fas 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_unit_name, $currentStockEntry->qu_unit_name_plural, true) }}
@if($currentStockEntry->amount_opened_aggregated > 0) @if($currentStockEntry->amount_opened_aggregated > 0)