Implemented stock sub-feature-flags (closes #314)

This commit is contained in:
Bernd Bestel
2019-09-19 17:46:52 +02:00
parent 5e9a7fb7ca
commit cbf1d1ca40
16 changed files with 164 additions and 50 deletions

View File

@@ -7,7 +7,7 @@
- Available on any barcode-enabled field (so currently only for picking products) - a new camera button at the right of side the text field - Available on any barcode-enabled field (so currently only for picking products) - a new camera button at the right of side the text field
- Implemented using [QuaggaJS](https://github.com/serratus/quaggaJS) - camera stream processing happens totally offline / client-side - Implemented using [QuaggaJS](https://github.com/serratus/quaggaJS) - camera stream processing happens totally offline / client-side
- Please note due to browser security restrictions, this only works when serving grocy via a secure connection (`https://`) - Please note due to browser security restrictions, this only works when serving grocy via a secure connection (`https://`)
- There is also a `config.php` setting `DISABLE_BROWSER_BARCODE_CAMERA_SCANNING` to disable this, if you don't need it at all - There is also a `config.php` setting `DISABLE_BROWSER_BARCODE_CAMERA_SCANNING` to disable this, if you don't need it at all (defaults to `false`)
### Stock improvements ### Stock improvements
- Products can now have variations (nested products) - Products can now have variations (nested products)
@@ -21,6 +21,11 @@
- It's now possible to print a "Location Content Sheet" with the current stock per location - new button at the top of the stock overview page (thought to hang it at the location, note used amounts on paper and track it in grocy later) - It's now possible to print a "Location Content Sheet" with the current stock per location - new button at the top of the stock overview page (thought to hang it at the location, note used amounts on paper and track it in grocy later)
- The product description now can have formattings (HTML/WYSIWYG editor like for recipes) - The product description now can have formattings (HTML/WYSIWYG editor like for recipes)
- "Factor purchase to stock quantity unit" (product option) can now also be a decimal number when "Allow partial units in stock" is enabled - "Factor purchase to stock quantity unit" (product option) can now also be a decimal number when "Allow partial units in stock" is enabled
- New "Sub feature flags" in `config.php` to disable some sub-features (hide the corresponding UI elements) if you don't need them (all new feature flags default to `true`, so no changed behaviour when not configured)
- `FEATURE_FLAG_STOCK_PRICE_TRACKING` to disable product price tracking
- `FEATURE_FLAG_STOCK_LOCATION_TRACKING` to disable product location tracking
- `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING` to disable product best before date tracking
- `FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING` to disable product opened tracking
### Recipe improvements ### Recipe improvements
- Based on the new linked quantity units, recipe ingredients can now use any product related unit, the amount is calculated according to the cnoversion factor of the unit relation - Based on the new linked quantity units, recipe ingredients can now use any product related unit, the amount is calculated according to the cnoversion factor of the unit relation

View File

@@ -17,8 +17,8 @@
# Either "production", "dev" or "prerelease" # Either "production", "dev" or "prerelease"
Setting('MODE', 'production'); Setting('MODE', 'production');
# Either "en" or "de" or the filename (without extension) of # Either "en" or "de" or the directory name of
# one of the other available localization files in the "/localization" directory # one of the other available localization folders in the "/localization" directory
Setting('CULTURE', 'en'); Setting('CULTURE', 'en');
# This is used to define the first day of a week for calendar views in the frontend, # This is used to define the first day of a week for calendar views in the frontend,
@@ -45,12 +45,11 @@ Setting('BASE_URL', '/');
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation # see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting, # If, however, your webserver does not support URL rewriting, set this to true
# set this to true
Setting('DISABLE_URL_REWRITING', false); Setting('DISABLE_URL_REWRITING', false);
# Specify an custom homepage if desired. By default the homepage will be set to the stock overview. # Specify an custom homepage if desired - by default the homepage will be set to the stock overview,
# You can chosen one of the following values: # this needs to be one of the following values:
# stock, shoppinglist, recipes, chores, tasks, batteries, equipment, calendar # stock, shoppinglist, recipes, chores, tasks, batteries, equipment, calendar
Setting('ENTRY_PAGE', 'stock'); Setting('ENTRY_PAGE', 'stock');
@@ -61,6 +60,7 @@ Setting('DISABLE_AUTH', false);
# Set this to true if you want to disable the ability to scan a barcode via the device camera (Browser API) # Set this to true if you want to disable the ability to scan a barcode via the device camera (Browser API)
Setting('DISABLE_BROWSER_BARCODE_CAMERA_SCANNING', false); Setting('DISABLE_BROWSER_BARCODE_CAMERA_SCANNING', false);
# Default user settings # Default user settings
# These settings can be changed per user, here the defaults # These settings can be changed per user, here the defaults
# are defined which are used when the user has not changed the setting so far # are defined which are used when the user has not changed the setting so far
@@ -103,8 +103,6 @@ DefaultUserSetting('show_clock_in_header', false);
DefaultUserSetting('shopping_list_to_stock_workflow_auto_submit_when_prefilled', false); DefaultUserSetting('shopping_list_to_stock_workflow_auto_submit_when_prefilled', false);
# Feature flags # Feature flags
# grocy was initially about "stock management for your household", many other things # grocy was initially about "stock management for your household", many other things
# came and still come by, because they are useful - here you can disable the parts # came and still come by, because they are useful - here you can disable the parts
@@ -118,3 +116,9 @@ Setting('FEATURE_FLAG_TASKS', true);
Setting('FEATURE_FLAG_BATTERIES', true); Setting('FEATURE_FLAG_BATTERIES', true);
Setting('FEATURE_FLAG_EQUIPMENT', true); Setting('FEATURE_FLAG_EQUIPMENT', true);
Setting('FEATURE_FLAG_CALENDAR', true); Setting('FEATURE_FLAG_CALENDAR', true);
# Sub feature flags
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);

View File

@@ -100,35 +100,38 @@ Grocy.Components.ProductCard.Refresh = function(productId)
} }
); );
Grocy.Api.Get('stock/products/' + productId + '/price-history', if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
function(priceHistoryDataPoints) {
{ Grocy.Api.Get('stock/products/' + productId + '/price-history',
if (priceHistoryDataPoints.length > 0) function(priceHistoryDataPoints)
{ {
$("#productcard-product-price-history-chart").removeClass("d-none"); if (priceHistoryDataPoints.length > 0)
$("#productcard-no-price-data-hint").addClass("d-none");
Grocy.Components.ProductCard.ReInitPriceHistoryChart();
priceHistoryDataPoints.forEach((dataPoint) =>
{ {
Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate()); $("#productcard-product-price-history-chart").removeClass("d-none");
$("#productcard-no-price-data-hint").addClass("d-none");
var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0]; Grocy.Components.ProductCard.ReInitPriceHistoryChart();
dataset.data.push(dataPoint.price); priceHistoryDataPoints.forEach((dataPoint) =>
}); {
Grocy.Components.ProductCard.PriceHistoryChart.update(); Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate());
}
else var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0];
dataset.data.push(dataPoint.price);
});
Grocy.Components.ProductCard.PriceHistoryChart.update();
}
else
{
$("#productcard-product-price-history-chart").addClass("d-none");
$("#productcard-no-price-data-hint").removeClass("d-none");
}
},
function(xhr)
{ {
$("#productcard-product-price-history-chart").addClass("d-none"); console.error(xhr);
$("#productcard-no-price-data-hint").removeClass("d-none");
} }
}, );
function(xhr) }
{
console.error(xhr);
}
);
}; };
Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() Grocy.Components.ProductCard.ReInitPriceHistoryChart = function()

View File

@@ -17,7 +17,14 @@
var jsonData = { }; var jsonData = { };
jsonData.new_amount = jsonForm.new_amount; jsonData.new_amount = jsonForm.new_amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
}
else
{
jsonData.location_id = 1;
}
jsonData.price = price; jsonData.price = price;
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/inventory', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/inventory', jsonData,
@@ -120,7 +127,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
Grocy.Components.LocationPicker.SetId(productDetails.location.id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
$('#new_amount').focus(); $('#new_amount').focus();
}, },
function(xhr) function(xhr)
@@ -221,13 +231,24 @@ $('#new_amount').on('keyup', function(e)
{ {
$('#inventory-change-info').text(__t('This means %s will be added to stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural))); $('#inventory-change-info').text(__t('This means %s will be added to stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)));
Grocy.Components.DateTimePicker.GetInputElement().attr('required', ''); Grocy.Components.DateTimePicker.GetInputElement().attr('required', '');
Grocy.Components.LocationPicker.GetInputElement().attr('required', ''); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
Grocy.Components.LocationPicker.GetInputElement().attr('required', '');
}
} }
else if (newAmount < productStockAmount + containerWeight) else if (newAmount < productStockAmount + containerWeight)
{ {
$('#inventory-change-info').text(__t('This means %s will be removed from stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural))); $('#inventory-change-info').text(__t('This means %s will be removed from stock', estimatedBookingAmount.toLocaleString() + ' ' + __n(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)));
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required'); Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
Grocy.Components.LocationPicker.GetInputElement().removeAttr('required'); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
Grocy.Components.LocationPicker.GetInputElement().removeAttr('required');
}
}
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
} }
Grocy.FrontendHelpers.ValidateForm('inventory-form'); Grocy.FrontendHelpers.ValidateForm('inventory-form');

View File

@@ -20,7 +20,14 @@
jsonData.amount = amount; jsonData.amount = amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.price = price; jsonData.price = price;
jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
}
else
{
jsonData.location_id = 1;
}
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
function(result) function(result)
@@ -73,7 +80,10 @@
$('#price').val(''); $('#price').val('');
$('#amount_qu_unit').text(""); $('#amount_qu_unit').text("");
$("#tare-weight-handling-info").addClass("d-none"); $("#tare-weight-handling-info").addClass("d-none");
Grocy.Components.LocationPicker.Clear(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
Grocy.Components.LocationPicker.Clear();
}
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
@@ -107,7 +117,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
function (productDetails) function (productDetails)
{ {
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
Grocy.Components.LocationPicker.SetId(productDetails.location.id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
{
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock) if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock)
{ {
@@ -166,7 +179,15 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
else else
{ {
Grocy.Components.DateTimePicker.GetInputElement().focus(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
Grocy.Components.DateTimePicker.GetInputElement().focus();
}
else
{
Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD'));
$('#amount').focus();
}
} }
}, },
function(xhr) function(xhr)

View File

@@ -13,8 +13,9 @@
@php if(!isset($noNameAttribute)) { $noNameAttribute = false; } @endphp @php if(!isset($noNameAttribute)) { $noNameAttribute = false; } @endphp
@php if(!isset($nextInputSelector)) { $nextInputSelector = false; } @endphp @php if(!isset($nextInputSelector)) { $nextInputSelector = false; } @endphp
@php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp @php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp
@php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp
<div class="form-group"> <div class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">{{ $__t($label) }} <label for="{{ $id }}">{{ $__t($label) }}
<span class="small text-muted"> <span class="small text-muted">
@if(!empty($hint)){{ $__t($hint) }}@endif @if(!empty($hint)){{ $__t($hint) }}@endif

View File

@@ -20,19 +20,21 @@
<strong>{{ $__t('Stock amount') . ' / ' . $__t('Quantity unit') }}:</strong> <span id="productcard-product-stock-amount"></span> <span id="productcard-product-stock-qu-name"></span> <span id="productcard-product-stock-opened-amount" class="small font-italic"></span> <strong>{{ $__t('Stock amount') . ' / ' . $__t('Quantity unit') }}:</strong> <span id="productcard-product-stock-amount"></span> <span id="productcard-product-stock-qu-name"></span> <span id="productcard-product-stock-opened-amount" class="small font-italic"></span>
<span id="productcard-aggregated-amounts" class="pl-2 text-secondary d-none"><i class="fas fa-custom-sigma-sign"></i> <span id="productcard-product-stock-amount-aggregated"></span> <span id="productcard-product-stock-qu-name-aggregated"></span> <span id="productcard-product-stock-opened-amount-aggregated" class="small font-italic"></span></span><br> <span id="productcard-aggregated-amounts" class="pl-2 text-secondary d-none"><i class="fas fa-custom-sigma-sign"></i> <span id="productcard-product-stock-amount-aggregated"></span> <span id="productcard-product-stock-qu-name-aggregated"></span> <span id="productcard-product-stock-opened-amount-aggregated" class="small font-italic"></span></span><br>
<strong>{{ $__t('Location') }}:</strong> <span id="productcard-product-location"></span><br> @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<strong>{{ $__t('Location') }}:</strong> <span id="productcard-product-location"></span><br>@endif
<strong>{{ $__t('Last purchased') }}:</strong> <span id="productcard-product-last-purchased"></span> <time id="productcard-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br> <strong>{{ $__t('Last purchased') }}:</strong> <span id="productcard-product-last-purchased"></span> <time id="productcard-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br> <strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br> @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br>@endif
<strong>{{ $__t('Average shelf life') }}:</strong> <span id="productcard-product-average-shelf-life"></span><br> @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)<strong>{{ $__t('Average shelf life') }}:</strong> <span id="productcard-product-average-shelf-life"></span><br>@endif
<strong>{{ $__t('Spoil rate') }}:</strong> <span id="productcard-product-spoil-rate"></span> <strong>{{ $__t('Spoil rate') }}:</strong> <span id="productcard-product-spoil-rate"></span>
<h5 class="mt-3">{{ $__t('Product picture') }}</h5> <h5 class="mt-3">{{ $__t('Product picture') }}</h5>
<p class="w-75 mx-auto"><img id="productcard-product-picture" data-src="" class="img-fluid img-thumbnail d-none lazy"></p> <p class="w-75 mx-auto"><img id="productcard-product-picture" data-src="" class="img-fluid img-thumbnail d-none lazy"></p>
<span id="productcard-no-product-picture" class="font-italic d-none">{{ $__t('No picture available') }}</span> <span id="productcard-no-product-picture" class="font-italic d-none">{{ $__t('No picture available') }}</span>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<h5 class="mt-3">{{ $__t('Price history') }}</h5> <h5 class="mt-3">{{ $__t('Price history') }}</h5>
<canvas id="productcard-product-price-history-chart" class="w-100 d-none"></canvas> <canvas id="productcard-product-price-history-chart" class="w-100 d-none"></canvas>
<span id="productcard-no-price-data-hint" class="font-italic d-none">{{ $__t('No price history available') }}</span> <span id="productcard-no-price-data-hint" class="font-italic d-none">{{ $__t('No price history available') }}</span>
@endif
</div> </div>
</div> </div>

View File

@@ -52,7 +52,10 @@
@endif @endif
<button id="save-consume-button" class="btn btn-success">{{ $__t('OK') }}</button> <button id="save-consume-button" class="btn btn-success">{{ $__t('OK') }}</button>
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<button id="save-mark-as-open-button" class="btn btn-secondary">{{ $__t('Mark as opened') }}</button> <button id="save-mark-as-open-button" class="btn btn-secondary">{{ $__t('Mark as opened') }}</button>
@endif
</form> </form>
</div> </div>

View File

@@ -28,6 +28,13 @@
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-small text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>' 'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-small text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
)) ))
@php
$additionalGroupCssClasses = '';
if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
$additionalGroupCssClasses = 'd-none';
}
@endphp
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'label' => 'Best before', 'label' => 'Best before',
@@ -42,9 +49,12 @@
'shortcutValue' => '2999-12-31', 'shortcutValue' => '2999-12-31',
'shortcutLabel' => 'Never expires', 'shortcutLabel' => 'Never expires',
'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoLimit' => date('Y-m-d'),
'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?') 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'),
'additionalGroupCssClasses' => $additionalGroupCssClasses
)) ))
@php $additionalGroupCssClasses = ''; @endphp
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'price', 'id' => 'price',
'label' => 'Price', 'label' => 'Price',
@@ -56,11 +66,18 @@
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
'isRequired' => false 'isRequired' => false
)) ))
@else
<input type="hidden" name="price" id="price" value="0">
@endif
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@include('components.locationpicker', array( @include('components.locationpicker', array(
'locations' => $locations, 'locations' => $locations,
'hint' => $__t('This will apply to added products') 'hint' => $__t('This will apply to added products')
)) ))
@else
<input type="hidden" name="location_id" id="location_id" value="1">
@endif
<button id="save-inventory-button" class="btn btn-success">{{ $__t('OK') }}</button> <button id="save-inventory-button" class="btn btn-success">{{ $__t('OK') }}</button>

View File

@@ -212,12 +212,14 @@
<span class="nav-link-text">{{ $__t('Products') }}</span> <span class="nav-link-text">{{ $__t('Products') }}</span>
</a> </a>
</li> </li>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<li data-nav-for-page="locations" data-sub-menu-of="#top-nav-manager-master-data"> <li data-nav-for-page="locations" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/locations') }}"> <a class="nav-link discrete-link" href="{{ $U('/locations') }}">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
<span class="nav-link-text">{{ $__t('Locations') }}</span> <span class="nav-link-text">{{ $__t('Locations') }}</span>
</a> </a>
</li> </li>
@endif
<li data-nav-for-page="quantityunits" data-sub-menu-of="#top-nav-manager-master-data"> <li data-nav-for-page="quantityunits" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}"> <a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}">
<i class="fas fa-balance-scale"></i> <i class="fas fa-balance-scale"></i>

View File

@@ -73,6 +73,7 @@
<div id="barcode-taginput-container"></div> <div id="barcode-taginput-container"></div>
</div> </div>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group"> <div class="form-group">
<label for="location_id">{{ $__t('Location') }}</label> <label for="location_id">{{ $__t('Location') }}</label>
<select required class="form-control" id="location_id" name="location_id"> <select required class="form-control" id="location_id" name="location_id">
@@ -83,6 +84,9 @@
</select> </select>
<div class="invalid-feedback">{{ $__t('A location is required') }}</div> <div class="invalid-feedback">{{ $__t('A location is required') }}</div>
</div> </div>
@else
<input type="hidden" name="location_id" id="location_id" value="1">
@endif
@php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp @php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(

View File

@@ -45,7 +45,7 @@
<tr> <tr>
<th class="border-right"></th> <th class="border-right"></th>
<th>{{ $__t('Name') }}</th> <th>{{ $__t('Name') }}</th>
<th>{{ $__t('Location') }}</th> <th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">{{ $__t('Location') }}</th>
<th>{{ $__t('Min. stock amount') }}</th> <th>{{ $__t('Min. stock amount') }}</th>
<th>{{ $__t('QU purchase') }}</th> <th>{{ $__t('QU purchase') }}</th>
<th>{{ $__t('QU stock') }}</th> <th>{{ $__t('QU stock') }}</th>
@@ -73,7 +73,7 @@
<td> <td>
{{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif {{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td> </td>
<td> <td class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }} {{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}
</td> </td>
<td> <td>

View File

@@ -16,6 +16,13 @@
'nextInputSelector' => '#best_before_date .datetimepicker-input' 'nextInputSelector' => '#best_before_date .datetimepicker-input'
)) ))
@php
$additionalGroupCssClasses = '';
if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{
$additionalGroupCssClasses = 'd-none';
}
@endphp
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'label' => 'Best before', 'label' => 'Best before',
@@ -29,8 +36,10 @@
'shortcutValue' => '2999-12-31', 'shortcutValue' => '2999-12-31',
'shortcutLabel' => 'Never expires', 'shortcutLabel' => 'Never expires',
'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoLimit' => date('Y-m-d'),
'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?') 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'),
'additionalGroupCssClasses' => $additionalGroupCssClasses
)) ))
@php $additionalGroupCssClasses = ''; @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'amount', 'id' => 'amount',
@@ -41,6 +50,7 @@
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>' 'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
)) ))
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'price', 'id' => 'price',
'label' => 'Price', 'label' => 'Price',
@@ -51,11 +61,18 @@
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
'isRequired' => false 'isRequired' => false
)) ))
@else
<input type="hidden" name="price" id="price" value="0">
@endif
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@include('components.locationpicker', array( @include('components.locationpicker', array(
'locations' => $locations, 'locations' => $locations,
'isRequired' => false 'isRequired' => false
)) ))
@else
<input type="hidden" name="location_id" id="location_id" value="1">
@endif
<button id="save-purchase-button" class="btn btn-success">{{ $__t('OK') }}</button> <button id="save-purchase-button" class="btn btn-success">{{ $__t('OK') }}</button>

View File

@@ -155,6 +155,7 @@
'additionalAttributes' => 'data-recipe-id="' . $selectedRecipe->id . '"' 'additionalAttributes' => 'data-recipe-id="' . $selectedRecipe->id . '"'
)) ))
</div> </div>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="col-7"> <div class="col-7">
<label>{{ $__t('Costs') }}&nbsp;&nbsp; <label>{{ $__t('Costs') }}&nbsp;&nbsp;
<span class="small text-muted">{{ $__t('Based on the prices of the last purchase per product') }}</span> <span class="small text-muted">{{ $__t('Based on the prices of the last purchase per product') }}</span>
@@ -163,6 +164,7 @@
<span class="locale-number-format" data-format="currency">{{ $selectedRecipeTotalCosts }}</span> <span class="locale-number-format" data-format="currency">{{ $selectedRecipeTotalCosts }}</span>
</p> </p>
</div> </div>
@endif
</div> </div>
</div> </div>

View File

@@ -24,12 +24,16 @@
<a class="btn btn-outline-dark responsive-button" href="{{ $U('/stockjournal') }}"> <a class="btn btn-outline-dark responsive-button" href="{{ $U('/stockjournal') }}">
<i class="fas fa-file-alt"></i> {{ $__t('Journal') }} <i class="fas fa-file-alt"></i> {{ $__t('Journal') }}
</a> </a>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<a class="btn btn-outline-dark responsive-button" href="{{ $U('/locationcontentsheet') }}"> <a class="btn btn-outline-dark responsive-button" href="{{ $U('/locationcontentsheet') }}">
<i class="fas fa-print"></i> {{ $__t('Location Content Sheet') }} <i class="fas fa-print"></i> {{ $__t('Location Content Sheet') }}
</a> </a>
@endif
</h1> </h1>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<p id="info-expiring-products" data-next-x-days="{{ $nextXDays }}" data-status-filter="expiring" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p> <p id="info-expiring-products" data-next-x-days="{{ $nextXDays }}" data-status-filter="expiring" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p>
<p id="info-expired-products" data-status-filter="expired" class="btn btn-lg btn-danger status-filter-button responsive-button mr-2"></p> <p id="info-expired-products" data-status-filter="expired" class="btn btn-lg btn-danger status-filter-button responsive-button mr-2"></p>
@endif
<p id="info-missing-products" data-status-filter="belowminstockamount" class="btn btn-lg btn-info status-filter-button responsive-button"></p> <p id="info-missing-products" data-status-filter="belowminstockamount" class="btn btn-lg btn-info status-filter-button responsive-button"></p>
</div> </div>
</div> </div>
@@ -39,6 +43,7 @@
<label for="search">{{ $__t('Search') }}</label> <i class="fas fa-search"></i> <label for="search">{{ $__t('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search"> <input type="text" class="form-control" id="search">
</div> </div>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="col-xs-12 col-md-6 col-xl-3"> <div class="col-xs-12 col-md-6 col-xl-3">
<label for="location-filter">{{ $__t('Filter by location') }}</label> <i class="fas fa-filter"></i> <label for="location-filter">{{ $__t('Filter by location') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="location-filter"> <select class="form-control" id="location-filter">
@@ -48,6 +53,7 @@
@endforeach @endforeach
</select> </select>
</div> </div>
@endif
<div class="col-xs-12 col-md-6 col-xl-3"> <div class="col-xs-12 col-md-6 col-xl-3">
<label for="location-filter">{{ $__t('Filter by product group') }}</label> <i class="fas fa-filter"></i> <label for="location-filter">{{ $__t('Filter by product group') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="product-group-filter"> <select class="form-control" id="product-group-filter">
@@ -61,8 +67,10 @@
<label for="status-filter">{{ $__t('Filter by status') }}</label> <i class="fas fa-filter"></i> <label for="status-filter">{{ $__t('Filter by status') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="status-filter"> <select class="form-control" id="status-filter">
<option class="bg-white" value="all">{{ $__t('All') }}</option> <option class="bg-white" value="all">{{ $__t('All') }}</option>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<option class="bg-warning" value="expiring">{{ $__t('Expiring soon') }}</option> <option class="bg-warning" value="expiring">{{ $__t('Expiring soon') }}</option>
<option class="bg-danger" value="expired">{{ $__t('Already expired') }}</option> <option class="bg-danger" value="expired">{{ $__t('Already expired') }}</option>
@endif
<option class="bg-info" value="belowminstockamount">{{ $__t('Below min. stock amount') }}</option> <option class="bg-info" value="belowminstockamount">{{ $__t('Below min. stock amount') }}</option>
</select> </select>
</div> </div>
@@ -89,7 +97,7 @@
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach($currentStock as $currentStockEntry) @foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif"> <tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}" <a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
@@ -105,12 +113,14 @@
data-consume-amount="{{ $currentStockEntry->amount }}"> data-consume-amount="{{ $currentStockEntry->amount }}">
<i class="fas fa-utensils"></i> {{ $__t('All') }} <i class="fas fa-utensils"></i> {{ $__t('All') }}
</a> </a>
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < 1 || $currentStockEntry->amount == $currentStockEntry->amount_opened) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Mark %1$s of %2$s as open', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}" <a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < 1 || $currentStockEntry->amount == $currentStockEntry->amount_opened) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Mark %1$s of %2$s as open', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}" data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"> data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}">
<i class="fas fa-box-open"></i> 1 <i class="fas fa-box-open"></i> 1
</a> </a>
@endif
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary" type="button" data-toggle="dropdown"> <button class="btn btn-sm btn-light text-secondary" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>

View File

@@ -12,6 +12,7 @@
<div id="productpresets"> <div id="productpresets">
<h4>{{ $__t('Presets for new products') }}</h4> <h4>{{ $__t('Presets for new products') }}</h4>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group"> <div class="form-group">
<label for="product_presets_location_id">{{ $__t('Location') }}</label> <label for="product_presets_location_id">{{ $__t('Location') }}</label>
<select class="form-control user-setting-control" id="product_presets_location_id" data-setting-key="product_presets_location_id"> <select class="form-control user-setting-control" id="product_presets_location_id" data-setting-key="product_presets_location_id">
@@ -21,6 +22,7 @@
@endforeach @endforeach
</select> </select>
</div> </div>
@endif
<div class="form-group"> <div class="form-group">
<label for="product_presets_product_group_id">{{ $__t('Product group') }}</label> <label for="product_presets_product_group_id">{{ $__t('Product group') }}</label>