mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +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:
|
||||
- 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
|
||||
- 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)
|
||||
- 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)
|
||||
|
@ -2350,3 +2350,9 @@ msgstr ""
|
||||
|
||||
msgid "Edit meal plan entry"
|
||||
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();
|
||||
$(".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>");
|
||||
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
|
||||
function(stockLocations)
|
||||
{
|
||||
var setDefault = 0;
|
||||
var stockAmountAtDefaultLocation = 0;
|
||||
stockLocations.forEach(stockLocation =>
|
||||
{
|
||||
if (productDetails.location.id == stockLocation.location_id)
|
||||
if (stockLocation.location_id == defaultLocationId)
|
||||
{
|
||||
$("#location_id").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||
}));
|
||||
$("#location_id").val(productDetails.location.id);
|
||||
$("#location_id").val(defaultLocationId);
|
||||
$("#location_id").trigger('change');
|
||||
setDefault = 1;
|
||||
stockAmountAtDefaultLocation += Number.parseFloat(stockLocation.amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -399,11 +407,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
|
||||
if (setDefault == 0)
|
||||
{
|
||||
$("#location_id").val(stockLocation.location_id);
|
||||
$("#location_id").val(defaultLocationId);
|
||||
$("#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")
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO: This check doesn't really check against products only at the given location
|
||||
// (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;
|
||||
$productStockAmount = SumArrayValue($potentialStockEntries, 'amount');
|
||||
if ($amount > $productStockAmount)
|
||||
{
|
||||
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';
|
||||
}
|
||||
|
||||
$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)
|
||||
{
|
||||
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;
|
||||
|
@ -128,11 +128,34 @@
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</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
|
||||
<input type="hidden"
|
||||
name="location_id"
|
||||
id="location_id"
|
||||
value="1">
|
||||
<input type="hidden"
|
||||
name="default_consume_location_id"
|
||||
id="default_consume_location_id"
|
||||
value="1">
|
||||
@endif
|
||||
|
||||
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->shopping_location_id; } @endphp
|
||||
|
Loading…
x
Reference in New Issue
Block a user