mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 17:45:39 +00:00
Implemented "default consume location" handling (closes #1365)
This commit is contained in:
parent
e69e7a9a9a
commit
61ed756dd0
@ -31,6 +31,13 @@
|
|||||||
- Product card, stock overiew and stock entries page optimizations regarding displaying prices:
|
- Product card, stock overiew and stock entries page optimizations regarding displaying prices:
|
||||||
- Prices are now shown per default purchase quantity unit, instead of per stock QU and when clicking/hovering, a tooltip shows the price per stock QU
|
- Prices are now shown per default purchase quantity unit, instead of per stock QU and when clicking/hovering, a tooltip shows the price per stock QU
|
||||||
- The price history chart is now based on the value per purchase QU, instead of per stock QU
|
- The price history chart is now based on the value per purchase QU, instead of per stock QU
|
||||||
|
- New product option "Default consume location" (not mandatory, defaults to not set / empty)
|
||||||
|
- When set, stock entries at that location will be consumed first
|
||||||
|
- => This will be automatically taken into account when consuming from the stock overview page and all other places where no specific location can be selected
|
||||||
|
- => On the consume page the location is preselected in the following order:
|
||||||
|
- 1. The new default consume location, if the product currently has any stock there, otherwise
|
||||||
|
- 2. The products default location, if the product currently has any stock there, otherwise
|
||||||
|
- 3. The first location where the product currently has any stock
|
||||||
- New product option "Disable own stock" (defaults to disabled)
|
- 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)
|
- 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)
|
||||||
- The location content sheet can now optionally list also out of stock products (at the products default location, new checkbox "Show only in-stock products " at the top of the page, defaults to enabled)
|
- The location content sheet can now optionally list also out of stock products (at the products default location, new checkbox "Show only in-stock products " at the top of the page, defaults to enabled)
|
||||||
|
@ -2350,3 +2350,9 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "Edit meal plan entry"
|
msgid "Edit meal plan entry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Default consume location"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stock entries at this location will be consumed first"
|
||||||
|
msgstr ""
|
||||||
|
55
migrations/0187.sql
Normal file
55
migrations/0187.sql
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
ADD default_consume_location_id INTEGER;
|
||||||
|
|
||||||
|
DROP VIEW stock_next_use;
|
||||||
|
CREATE VIEW stock_next_use
|
||||||
|
AS
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default consume rule is:
|
||||||
|
Opened first, then first due first, then first in first out
|
||||||
|
Apart from that products at their default consume location should be consumed first
|
||||||
|
|
||||||
|
This orders the stock entries by that
|
||||||
|
=> Highest "priority" per product = the stock entry to use next
|
||||||
|
*/
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(ROW_NUMBER() OVER(PARTITION BY s.product_id ORDER BY CASE WHEN IFNULL(p.default_consume_location_id, -1) = s.location_id THEN 0 ELSE 1 END ASC, s.open DESC, s.best_before_date ASC, s.purchased_date ASC)) * -1 AS priority,
|
||||||
|
s.*
|
||||||
|
FROM stock s
|
||||||
|
JOIN products p
|
||||||
|
ON p.id = s.product_id;
|
||||||
|
|
||||||
|
CREATE TRIGGER stock_next_use_INS INSTEAD OF INSERT ON stock_next_use
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO stock
|
||||||
|
(product_id, amount, best_before_date, purchased_date, stock_id,
|
||||||
|
price, open, opened_date, location_id, shopping_location_id, note)
|
||||||
|
VALUES
|
||||||
|
(NEW.product_id, NEW.amount, NEW.best_before_date, NEW.purchased_date, NEW.stock_id,
|
||||||
|
NEW.price, NEW.open, NEW.opened_date, NEW.location_id, NEW.shopping_location_id, NEW.note);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER stock_next_use_UPD INSTEAD OF UPDATE ON stock_next_use
|
||||||
|
BEGIN
|
||||||
|
UPDATE stock
|
||||||
|
SET product_id = NEW.product_id,
|
||||||
|
amount = NEW.amount,
|
||||||
|
best_before_date = NEW.best_before_date,
|
||||||
|
purchased_date = NEW.purchased_date,
|
||||||
|
stock_id = NEW.stock_id,
|
||||||
|
price = NEW.price,
|
||||||
|
open = NEW.open,
|
||||||
|
opened_date = NEW.opened_date,
|
||||||
|
location_id = NEW.location_id,
|
||||||
|
shopping_location_id = NEW.shopping_location_id,
|
||||||
|
note = NEW.note
|
||||||
|
WHERE id = NEW.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER stock_next_use_DEL INSTEAD OF DELETE ON stock_next_use
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM stock
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END;
|
@ -372,22 +372,30 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
RefreshLocaleNumberInput();
|
RefreshLocaleNumberInput();
|
||||||
$(".input-group-productamountpicker").trigger("change");
|
$(".input-group-productamountpicker").trigger("change");
|
||||||
|
|
||||||
|
var defaultLocationId = productDetails.location.id;
|
||||||
|
if (productDetails.product.default_consume_location_id != null && !productDetails.product.default_consume_location_id.isEmpty())
|
||||||
|
{
|
||||||
|
defaultLocationId = productDetails.product.default_consume_location_id;
|
||||||
|
}
|
||||||
|
|
||||||
$("#location_id").find("option").remove().end().append("<option></option>");
|
$("#location_id").find("option").remove().end().append("<option></option>");
|
||||||
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
|
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
|
||||||
function(stockLocations)
|
function(stockLocations)
|
||||||
{
|
{
|
||||||
var setDefault = 0;
|
var setDefault = 0;
|
||||||
|
var stockAmountAtDefaultLocation = 0;
|
||||||
stockLocations.forEach(stockLocation =>
|
stockLocations.forEach(stockLocation =>
|
||||||
{
|
{
|
||||||
if (productDetails.location.id == stockLocation.location_id)
|
if (stockLocation.location_id == defaultLocationId)
|
||||||
{
|
{
|
||||||
$("#location_id").append($("<option>", {
|
$("#location_id").append($("<option>", {
|
||||||
value: stockLocation.location_id,
|
value: stockLocation.location_id,
|
||||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||||
}));
|
}));
|
||||||
$("#location_id").val(productDetails.location.id);
|
$("#location_id").val(defaultLocationId);
|
||||||
$("#location_id").trigger('change');
|
$("#location_id").trigger('change');
|
||||||
setDefault = 1;
|
setDefault = 1;
|
||||||
|
stockAmountAtDefaultLocation += Number.parseFloat(stockLocation.amount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -399,11 +407,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
|
|
||||||
if (setDefault == 0)
|
if (setDefault == 0)
|
||||||
{
|
{
|
||||||
$("#location_id").val(stockLocation.location_id);
|
$("#location_id").val(defaultLocationId);
|
||||||
$("#location_id").trigger('change');
|
$("#location_id").trigger('change');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (stockAmountAtDefaultLocation == 0)
|
||||||
|
{
|
||||||
|
$("#location_id option")[1].selected = true;
|
||||||
|
$("#location_id").trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
if (document.getElementById("product_id").getAttribute("barcode") != "null")
|
if (document.getElementById("product_id").getAttribute("barcode") != "null")
|
||||||
{
|
{
|
||||||
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
|
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
|
||||||
|
@ -406,10 +406,7 @@ class StockService extends BaseService
|
|||||||
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This check doesn't really check against products only at the given location
|
$productStockAmount = SumArrayValue($potentialStockEntries, 'amount');
|
||||||
// (as GetProductDetails returns the stock_amount_aggregated of all locations)
|
|
||||||
// However, $potentialStockEntries are filtered accordingly, so this currently isn't really a problem at the end
|
|
||||||
$productStockAmount = ((object) $this->GetProductDetails($productId))->stock_amount_aggregated;
|
|
||||||
if ($amount > $productStockAmount)
|
if ($amount > $productStockAmount)
|
||||||
{
|
{
|
||||||
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
|
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
|
||||||
@ -823,13 +820,11 @@ class StockService extends BaseService
|
|||||||
$sqlWhereAndOpen = 'AND open = 0';
|
$sqlWhereAndOpen = 'AND open = 0';
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->getDatabase()->stock()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen);
|
$result = $this->getDatabase()->stock_next_use()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen);
|
||||||
|
|
||||||
// In order of next use:
|
|
||||||
// Opened first, then first due first, then first in first out
|
|
||||||
if ($ordered)
|
if ($ordered)
|
||||||
{
|
{
|
||||||
return $result->orderBy('open', 'DESC')->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC');
|
return $result->orderBy('product_id', 'ASC')->orderBy('priority', 'DESC');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
@ -128,11 +128,34 @@
|
|||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="default_consume_location_id">
|
||||||
|
{{ $__t('Default consume location') }}
|
||||||
|
<i class="fas fa-question-circle text-muted"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-trigger="hover click"
|
||||||
|
title="{{ $__t('Stock entries at this location will be consumed first') }}"></i>
|
||||||
|
</label>
|
||||||
|
<select class="custom-control custom-select"
|
||||||
|
id="default_consume_location_id"
|
||||||
|
name="default_consume_location_id">
|
||||||
|
<option></option>
|
||||||
|
@foreach($locations as $location)
|
||||||
|
<option @if($mode=='edit'
|
||||||
|
&&
|
||||||
|
$location->id == $product->default_consume_location_id) selected="selected" @endif value="{{ $location->id }}">{{ $location->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
@else
|
@else
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="location_id"
|
name="location_id"
|
||||||
id="location_id"
|
id="location_id"
|
||||||
value="1">
|
value="1">
|
||||||
|
<input type="hidden"
|
||||||
|
name="default_consume_location_id"
|
||||||
|
id="default_consume_location_id"
|
||||||
|
value="1">
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->shopping_location_id; } @endphp
|
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->shopping_location_id; } @endphp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user