mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 09:39:57 +00:00
Implement "Free products" (closes #426)
This commit is contained in:
parent
f09ba08549
commit
71a57c9dcb
@ -20,6 +20,12 @@
|
||||
- On using "Consume all ingredients needed by this recipe" and when it has a product attached, one unit of that product (per serving in purchase quantity unit) will be added to stock (with the proper price based on the recipe ingredients)
|
||||
- (Thanks @kriddles for the intial work on this)
|
||||
|
||||
### New feature: Freeze/Thaw products
|
||||
- New product options "Default best before days after freezing/thawing" to set how the best before date should be changed on freezing/thawing
|
||||
- New location option "Is freezer" to indicate if the location is a freezer
|
||||
- => When moving a product from/to a freezer location, the best before date is changed accordingly
|
||||
- There is also a new sub feature flag `FEATURE_FLAG_STOCK_PRODUCT_FREEZING` to disable this if you don't need it (defaults to `true`)
|
||||
|
||||
### Stock improvements/fixes
|
||||
- The productcard gets now also refreshed after a transaction was posted (purchase/consume/etc.) (thanks @kriddles)
|
||||
- The product field calories (kcal) now also allows decimal numbers
|
||||
|
@ -136,6 +136,7 @@ Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);
|
||||
Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true);
|
||||
Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true);
|
||||
Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true);
|
||||
Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true);
|
||||
Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true);
|
||||
Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true);
|
||||
|
||||
|
@ -3467,8 +3467,11 @@
|
||||
"location_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"location_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"location_is_freezer": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
@ -330,3 +330,6 @@ msgstr ""
|
||||
|
||||
msgid "This is a note"
|
||||
msgstr ""
|
||||
|
||||
msgid "Freezer"
|
||||
msgstr ""
|
||||
|
@ -1681,3 +1681,30 @@ msgstr ""
|
||||
|
||||
msgid "Scan mode is on but not all required fields could be populated automatically"
|
||||
msgstr ""
|
||||
|
||||
msgid "Is freezer"
|
||||
msgstr ""
|
||||
|
||||
msgid "When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "On moving this product to a freezer location, the best before date will be replaced by today + this amount of days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default best before days after freezing"
|
||||
msgstr ""
|
||||
|
||||
msgid "On moving this product from a freezer location, the best before date will be replaced by today + this amount of days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default best before days after thawing"
|
||||
msgstr ""
|
||||
|
||||
msgid "This cannot be the same as the \"From\" location"
|
||||
msgstr ""
|
||||
|
||||
msgid "Thawed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Frozen"
|
||||
msgstr ""
|
||||
|
31
migrations/0097.sql
Normal file
31
migrations/0097.sql
Normal file
@ -0,0 +1,31 @@
|
||||
ALTER TABLE products
|
||||
ADD default_best_before_days_after_freezing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE products
|
||||
SET default_best_before_days_after_freezing = 0;
|
||||
|
||||
ALTER TABLE products
|
||||
ADD default_best_before_days_after_thawing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE products
|
||||
SET default_best_before_days_after_thawing = 0;
|
||||
|
||||
ALTER TABLE locations
|
||||
ADD is_freezer TINYINT NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE locations
|
||||
SET is_freezer = 0;
|
||||
|
||||
DROP VIEW stock_current_locations;
|
||||
CREATE VIEW stock_current_locations
|
||||
AS
|
||||
SELECT
|
||||
1 AS id, -- Dummy, LessQL needs an id column
|
||||
s.product_id,
|
||||
s.location_id AS location_id,
|
||||
l.name AS location_name,
|
||||
l.is_freezer AS location_is_freezer
|
||||
FROM stock s
|
||||
JOIN locations l
|
||||
ON s.location_id = l.id
|
||||
GROUP BY s.product_id, s.location_id, l.name;
|
@ -1,4 +1,4 @@
|
||||
$('#save-transfer-button').on('click', function(e)
|
||||
$('#save-transfer-button').on('click', function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
@ -71,10 +71,18 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
|
||||
toastr.success(successMessage);
|
||||
|
||||
if (parseInt($("#location_id_from option:selected").attr("data-is-freezer")) === 0 && parseInt($("#location_id_to option:selected").attr("data-is-freezer")) === 1) // Frozen
|
||||
{
|
||||
toastr.info('<span>' + __t("Frozen") + "</span> <i class='fas fa-snowflake'></i>");
|
||||
}
|
||||
if (parseInt($("#location_id_from option:selected").attr("data-is-freezer")) === 1 && parseInt($("#location_id_to option:selected").attr("data-is-freezer")) === 0) // Thawed
|
||||
{
|
||||
toastr.info('<span>' + __t("Thawed") + "</span> <i class='fas fa-fire-alt'></i>");
|
||||
}
|
||||
|
||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||
$("#specific_stock_entry").attr("disabled", "");
|
||||
$("#specific_stock_entry").removeAttr("required");
|
||||
@ -151,7 +159,8 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
{
|
||||
$("#location_id_from").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||
text: stockLocation.location_name + " (" + __t("Default location") + ")",
|
||||
"data-is-freezer": stockLocation.location_is_freezer
|
||||
}));
|
||||
$("#location_id_from").val(productDetails.location.id);
|
||||
$("#location_id_from").trigger('change');
|
||||
@ -161,7 +170,8 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
{
|
||||
$("#location_id_from").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name
|
||||
text: stockLocation.location_name,
|
||||
"data-is-freezer": stockLocation.location_is_freezer
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ class DemoDataGeneratorService extends BaseService
|
||||
INSERT INTO locations (name) VALUES ('{$this->__t_sql('Pantry')}'); --3
|
||||
INSERT INTO locations (name) VALUES ('{$this->__t_sql('Candy cupboard')}'); --4
|
||||
INSERT INTO locations (name) VALUES ('{$this->__t_sql('Tinned food cupboard')}'); --5
|
||||
INSERT INTO locations (name, is_freezer) VALUES ('{$this->__t_sql('Freezer')}', 1); --6
|
||||
|
||||
DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Glass')}';
|
||||
INSERT INTO quantity_units (id, name, name_plural) VALUES (4, '{$this->__n_sql(1, 'Glass', 'Glasses')}', '{$this->__n_sql(2, 'Glass', 'Glasses')}'); --4
|
||||
@ -86,7 +87,7 @@ class DemoDataGeneratorService extends BaseService
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Sieved tomatoes')}', 5, 5, 5, 1, 3); --17
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Salami')}', 2, 3, 3, 1, 6); --18
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Toast')}', 3, 5, 5, 1, 2); --19
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Minced meat')}', 2, 3, 3, 1, 4); --20
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, default_best_before_days_after_freezing, default_best_before_days_after_thawing) VALUES ('{$this->__t_sql('Minced meat')}', 2, 3, 3, 1, 4, 180, 2); --20
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, enable_tare_weight_handling, tare_weight, calories) VALUES ('{$this->__t_sql('Flour')}', 3, 8, 8, 1, 3, 1, 500, 2); --21
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Sugar')}', 3, 3, 3, 1, 3, 4); --22
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Milk')}', 2, 10, 10, 1, 6, 1); --23
|
||||
|
@ -464,6 +464,26 @@ class StockService extends BaseService
|
||||
break;
|
||||
}
|
||||
|
||||
$newBestBeforeDate = $stockEntry->best_before_date;
|
||||
|
||||
if (GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING)
|
||||
{
|
||||
$locationFrom = $this->Database->locations()->where('id', $locationIdFrom)->fetch();
|
||||
$locationTo = $this->Database->locations()->where('id', $locationIdTo)->fetch();
|
||||
|
||||
// Product was moved from a non-freezer to freezer location -> freeze
|
||||
if (intval($locationFrom->is_freezer) === 0 && intval($locationTo->is_freezer) === 1 && $productDetails->product->default_best_before_days_after_freezing > 0)
|
||||
{
|
||||
$newBestBeforeDate = date("Y-m-d", strtotime('+' . $productDetails->product->default_best_before_days_after_freezing . ' days'));
|
||||
}
|
||||
|
||||
// Product was moved from a freezer to non-freezer location -> thaw
|
||||
if (intval($locationFrom->is_freezer) === 1 && intval($locationTo->is_freezer) === 0 && $productDetails->product->default_best_before_days_after_thawing > 0)
|
||||
{
|
||||
$newBestBeforeDate = date("Y-m-d", strtotime('+' . $productDetails->product->default_best_before_days_after_thawing . ' days'));
|
||||
}
|
||||
}
|
||||
|
||||
$correlationId = uniqid();
|
||||
if ($amount >= $stockEntry->amount) // Take the whole stock entry
|
||||
{
|
||||
@ -485,7 +505,7 @@ class StockService extends BaseService
|
||||
$logRowForLocationTo = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $stockEntry->amount,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'best_before_date' => $newBestBeforeDate,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO,
|
||||
@ -498,7 +518,8 @@ class StockService extends BaseService
|
||||
$logRowForLocationTo->save();
|
||||
|
||||
$stockEntry->update(array(
|
||||
'location_id' => $locationIdTo
|
||||
'location_id' => $locationIdTo,
|
||||
'best_before_date' => $newBestBeforeDate
|
||||
));
|
||||
|
||||
$amount -= $stockEntry->amount;
|
||||
@ -525,7 +546,7 @@ class StockService extends BaseService
|
||||
$logRowForLocationTo = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'best_before_date' => $newBestBeforeDate,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO,
|
||||
@ -546,7 +567,7 @@ class StockService extends BaseService
|
||||
$stockEntryNew = $this->Database->stock()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'best_before_date' => $newBestBeforeDate,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'price' => $stockEntry->price,
|
||||
|
@ -32,6 +32,20 @@
|
||||
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $location->description }}@endif</textarea>
|
||||
</div>
|
||||
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING)
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="hidden" name="is_freezer" value="0">
|
||||
<input @if($mode == 'edit' && $location->is_freezer == 1) checked @endif class="form-check-input" type="checkbox" id="is_freezer" name="is_freezer" value="1">
|
||||
<label class="form-check-label" for="is_freezer">{{ $__t('Is freezer') }}
|
||||
<span class="text-muted small">{{ $__t('When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="is_freezer" value="0">
|
||||
@endif
|
||||
|
||||
@include('components.userfieldsform', array(
|
||||
'userfields' => $userfields,
|
||||
'entity' => 'locations'
|
||||
|
@ -227,6 +227,31 @@
|
||||
))
|
||||
@endif
|
||||
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING)
|
||||
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_freezing; } else { $value = 0; } @endphp
|
||||
@include('components.numberpicker', array(
|
||||
'id' => 'default_best_before_days_after_freezing',
|
||||
'label' => 'Default best before days after freezing',
|
||||
'min' => -1,
|
||||
'value' => $value,
|
||||
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'),
|
||||
'hint' => $__t('On moving this product to a freezer location, the best before date will be replaced by today + this amount of days')
|
||||
))
|
||||
|
||||
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_thawing; } else { $value = 0; } @endphp
|
||||
@include('components.numberpicker', array(
|
||||
'id' => 'default_best_before_days_after_thawing',
|
||||
'label' => 'Default best before days after thawing',
|
||||
'min' => -1,
|
||||
'value' => $value,
|
||||
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'),
|
||||
'hint' => $__t('On moving this product from a freezer location, the best before date will be replaced by today + this amount of days')
|
||||
))
|
||||
@else
|
||||
<input type="hidden" name="default_best_before_days_after_freezing" value="0">
|
||||
<input type="hidden" name="default_best_before_days_after_thawing" value="0">
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product-picture">{{ $__t('Product picture') }}
|
||||
<span class="text-muted small">{{ $__t('If you don\'t select a file, the current picture will not be altered') }}</span>
|
||||
|
@ -29,7 +29,7 @@
|
||||
<select required class="form-control location-combobox" id="location_id_from" name="location_id_from">
|
||||
<option></option>
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||
<option value="{{ $location->id }}" data-is-freezer="{{ $location->is_freezer }}">{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||
@ -66,7 +66,7 @@
|
||||
<select required class="form-control location-combobox" id="location_id_to" name="location_id_to">
|
||||
<option></option>
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||
<option value="{{ $location->id }}" data-is-freezer="{{ $location->is_freezer }}">{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user