Distinguish expiry/best before dates (closes #851)

This commit is contained in:
Bernd Bestel 2020-11-15 19:53:44 +01:00
parent 1d50d5dd22
commit b393998601
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
39 changed files with 348 additions and 192 deletions

View File

@ -70,7 +70,7 @@ The following shorthands are available:
- Example: `20190417` will be converted to `2019-04-17` - Example: `20190417` will be converted to `2019-04-17`
- `YYYYMMe` or `YYYYMM+` gets expanded to the end of the given month in the given year in proper notation - `YYYYMMe` or `YYYYMM+` gets expanded to the end of the given month in the given year in proper notation
- Example: `201807e` will be converted to `2018-07-31` - Example: `201807e` will be converted to `2018-07-31`
- `x` gets expanded to `2999-12-31` (which I use for products which never expire) - `x` gets expanded to `2999-12-31` (which I use for products which are never overdue)
- Down/up arrow keys will increase/decrease the date by 1 day - Down/up arrow keys will increase/decrease the date by 1 day
- Right/left arrow keys will increase/decrease the date by 1 week - Right/left arrow keys will increase/decrease the date by 1 week
- Shift + down/up arrow keys will increase/decrease the date by 1 month - Shift + down/up arrow keys will increase/decrease the date by 1 month

View File

@ -29,13 +29,23 @@ _- (Because the stock quantity unit is now the base for everything, it cannot be
- (Thanks @fipwmaqzufheoxq92ebc for the initial work on this) - (Thanks @fipwmaqzufheoxq92ebc for the initial work on this)
### Stock improvements/fixes ### Stock improvements/fixes
- Changes about best before dates: It's now possible to distinguish between best before dates and expiration dates:
- New product option "Due date type" (defaults to "Best before date")
- Wording changes:
- All current places where "Best before date" was used now use "Due date"
- Products with `Due date type = Best before date` (so all existing products) are "due" or "overdue" (they don't "expire" or are "expired")
- Products with `Due date type = Expiration date` (new option) can "expire" or are "expired"
- Color changes:
- Prducts which are due soon or expire soon are (still) highlighted in yellow
- Products which are overdue are highlighted in grey (there is is also a new filter button on the stock overview page for them)
- Products which are expired (new option) are highlighted in red
- When creating a quantity unit conversion it's now possible to automatically create the inverse conversion (thanks @kriddles) - When creating a quantity unit conversion it's now possible to automatically create the inverse conversion (thanks @kriddles)
- On purchase there is now a warning shown, when the best before date of the purchased product is earlier than the next best before date in stock (enabled by default, can be disabled by a new stock setting (top right corner settings menu)) - On purchase there is now a warning shown, when the due date of the purchased product is earlier than the next due date in stock (enabled by default, can be disabled by a new stock setting (top right corner settings menu))
- The amount to be used for the "quick consume/open buttons" on the stock overview page can now be configured per product (new product option "Quick consume amount", defaults to 1) - The amount to be used for the "quick consume/open buttons" on the stock overview page can now be configured per product (new product option "Quick consume amount", defaults to 1)
- Products can now be duplicated (new button on the products list page, all fields will be preset from the copied product, except the name) - Products can now be duplicated (new button on the products list page, all fields will be preset from the copied product, except the name)
- Optimized/clarified what the total/unit price is on the purchase page (thanks @kriddles) - Optimized/clarified what the total/unit price is on the purchase page (thanks @kriddles)
- On the purchase page the amount field is now displayed above/before the best before date for better `TAB` handling (thanks @kriddles) - On the purchase page the amount field is now displayed above/before the due date for better `TAB` handling (thanks @kriddles)
- Changed that when `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING` is disabled, products now get internally a best before of "never expires" (aka `2999-12-31`) instead of today (thanks @kriddles) - Changed that when `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING` is disabled, products now get internally a due date of "never overdue" (aka `2999-12-31`) instead of today (thanks @kriddles)
- Products can now be hidden instead of deleted to prevent problems / missing information on existing references (new checkbox on the product edit page) (thanks @kriddles) - Products can now be hidden instead of deleted to prevent problems / missing information on existing references (new checkbox on the product edit page) (thanks @kriddles)
- On the stock journal page, it's now visible if a consume-booking was spoiled - On the stock journal page, it's now visible if a consume-booking was spoiled
- It's now tracked who made a stock change (currently logged in user, visible on the stock journal page) (thanks @fipwmaqzufheoxq92ebc) - It's now tracked who made a stock change (currently logged in user, visible on the stock journal page) (thanks @fipwmaqzufheoxq92ebc)
@ -53,7 +63,7 @@ _- (Because the stock quantity unit is now the base for everything, it cannot be
- Fixed that when adding products through a product picker workflow and when the created products contains special characters, the product was not preselected on the previous page (thanks @Forceu) - Fixed that when adding products through a product picker workflow and when the created products contains special characters, the product was not preselected on the previous page (thanks @Forceu)
- Fixed that when editing a product the default store was not visible / always empty regardless if the product had one set (thanks @kriddles) - Fixed that when editing a product the default store was not visible / always empty regardless if the product had one set (thanks @kriddles)
- Fixed that `FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT` (option to configure if opened products should be considered for minimum stock amounts) was not handled correctly (thanks @teddybeermaniac) - Fixed that `FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT` (option to configure if opened products should be considered for minimum stock amounts) was not handled correctly (thanks @teddybeermaniac)
- Fixed that the "Expiring soon" sum (yellow filter button) on the stock overview page didn't include products which expire today (thanks @fipwmaqzufheoxq92ebc) - Fixed that the "Due soon" sum (yellow filter button) on the stock overview page didn't include products which are due today (thanks @fipwmaqzufheoxq92ebc)
- Fixed that the shopping cart icon on the stock overview page was also shown if the product was on an already deleted shopping list (if enabled) (thanks @fipwmaqzufheoxq92ebc) - Fixed that the shopping cart icon on the stock overview page was also shown if the product was on an already deleted shopping list (if enabled) (thanks @fipwmaqzufheoxq92ebc)
- Fixed that when editing a stock entry without a price, the price field was prefilled with `1` - Fixed that when editing a stock entry without a price, the price field was prefilled with `1`
- Fixed that the location & product groups filter on the stock overview page used a contains search instead of an exact search - Fixed that the location & product groups filter on the stock overview page used a contains search instead of an exact search
@ -73,7 +83,7 @@ _- (Because the stock quantity unit is now the base for everything, it cannot be
### Recipe improvements/fixes ### Recipe improvements/fixes
- It's now possible to print recipes (button next to the recipe title) (thanks @zsarnett) - It's now possible to print recipes (button next to the recipe title) (thanks @zsarnett)
- Changed that recipe costs are now based on the costs of the products picked by the default consume rule "First expiring first, then first in first out" (thanks @kriddles) - Changed that recipe costs are now based on the costs of the products picked by the default consume rule "First due first, then first in first out" (thanks @kriddles)
- Recipe costs were based on the last purchase price per product before, so this now better reflects the current real costs - Recipe costs were based on the last purchase price per product before, so this now better reflects the current real costs
- Improved the recipe add workflow (a recipe called "New recipe" is now not automatically created when starting to add a recipe) (thanks @zsarnett) - Improved the recipe add workflow (a recipe called "New recipe" is now not automatically created when starting to add a recipe) (thanks @zsarnett)
- Fixed that images on the recipe gallery view were not scaled correctly on larger screens (thanks @zsarnett) - Fixed that images on the recipe gallery view were not scaled correctly on larger screens (thanks @zsarnett)
@ -145,6 +155,11 @@ _- (Because the stock quantity unit is now the base for everything, it cannot be
- The product object no longer has a field `barcodes` with a comma separated barcode list, instead barcodes are now stored in a separate table/entity `product_barcodes` (use the existing "Generic entity interactions" endpoints to access them) - The product object no longer has a field `barcodes` with a comma separated barcode list, instead barcodes are now stored in a separate table/entity `product_barcodes` (use the existing "Generic entity interactions" endpoints to access them)
- The endpoint `/objects/{entity}/search` was removed (use the existing `/objects/{entity}` endpoint with new new filter capabilities mentioned below) - The endpoint `/objects/{entity}/search` was removed (use the existing `/objects/{entity}` endpoint with new new filter capabilities mentioned below)
- The output / field names of `ProductDetailsResponse` have slightly changed (endpoint `/stock/products/{productId}`) - The output / field names of `ProductDetailsResponse` have slightly changed (endpoint `/stock/products/{productId}`)
- Endpoint `/stock/volatile`
- The query parameter `expring_days` was renamed to `due_soon_days`
- The field `expiring_products` was renamed to `due_products`
- The field `expired_products` now only contains expired products (so them with `Due date type = Expiration date`)
- The new field `overdue_products` contains only overdue products (so them with `Due date type = Best before date`)
- For better integration (apps), it's now possible to show a QR-Code for API keys (thanks @fipwmaqzufheoxq92ebc) - For better integration (apps), it's now possible to show a QR-Code for API keys (thanks @fipwmaqzufheoxq92ebc)
- New QR-Code button on the "Manage API keys"-page (top right corner settings menu), the QR-Codes contains `<API-Url>|<API-Key>` - New QR-Code button on the "Manage API keys"-page (top right corner settings menu), the QR-Codes contains `<API-Url>|<API-Key>`
- And on the calendar page when using the button "Share/Integrate calendar (iCal)", there the QR-Codes contains the Share-URL (which is displayed in the textbox above) - And on the calendar page when using the button "Share/Integrate calendar (iCal)", there the QR-Codes contains the Share-URL (which is displayed in the textbox above)
@ -174,7 +189,7 @@ _- (Because the stock quantity unit is now the base for everything, it cannot be
- `<=` less or equal - `<=` less or equal
- `<value>` is the value to search for - `<value>` is the value to search for
- New endpoints `/stock/journal` and `/stock/journal/summary` to get the stock journal (thanks @fipwmaqzufheoxq92ebc) - New endpoints `/stock/journal` and `/stock/journal/summary` to get the stock journal (thanks @fipwmaqzufheoxq92ebc)
- New endpoint `/stock/shoppinglist/add-expired-products` to add all currently in-stock but expired products to a shopping list (thanks @m-byte) - New endpoint `/stock/shoppinglist/add-overdue-products` to add all currently in-stock but overdue products to a shopping list (thanks @m-byte)
- New endpoints GET/POST/PUT `/users/{userId}/permissions` for the new user permissions feature mentioned above - New endpoints GET/POST/PUT `/users/{userId}/permissions` for the new user permissions feature mentioned above
- Performance improvements of the `/stock/products/*` endpoints (thanks @fipwmaqzufheoxq92ebc) - Performance improvements of the `/stock/products/*` endpoints (thanks @fipwmaqzufheoxq92ebc)
- Fixed that the endpoint `/objects/{entity}/{objectId}` always returned successfully, even when the given object not exists (now returns `404` when the object is not found) (thanks @fipwmaqzufheoxq92ebc) - Fixed that the endpoint `/objects/{entity}/{objectId}` always returned successfully, even when the given object not exists (now returns `404` when the object is not found) (thanks @fipwmaqzufheoxq92ebc)

View File

@ -113,17 +113,17 @@ DefaultUserSetting('product_presets_product_group_id', -1); // Default product g
DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset) DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset)
DefaultUserSetting('stock_decimal_places_amounts', 4); // Default decimal places allowed for amounts DefaultUserSetting('stock_decimal_places_amounts', 4); // Default decimal places allowed for amounts
DefaultUserSetting('stock_decimal_places_prices', 2); // Default decimal places allowed for prices DefaultUserSetting('stock_decimal_places_prices', 2); // Default decimal places allowed for prices
DefaultUserSetting('stock_expiring_soon_days', 5); DefaultUserSetting('stock_due_soon_days', 5);
DefaultUserSetting('stock_default_purchase_amount', 0); DefaultUserSetting('stock_default_purchase_amount', 0);
DefaultUserSetting('stock_default_consume_amount', 1); DefaultUserSetting('stock_default_consume_amount', 1);
DefaultUserSetting('scan_mode_consume_enabled', false); DefaultUserSetting('scan_mode_consume_enabled', false);
DefaultUserSetting('scan_mode_purchase_enabled', false); DefaultUserSetting('scan_mode_purchase_enabled', false);
DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true); DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true);
DefaultUserSetting('show_purchased_date_on_purchase', false); // Wheter the purchased date should be editable on purchase (defaults to today otherwise) DefaultUserSetting('show_purchased_date_on_purchase', false); // Wheter the purchased date should be editable on purchase (defaults to today otherwise)
DefaultUserSetting('show_warning_on_purchase_when_best_before_date_is_earlier_than_next', true); // Show a warning on purchase when the best before date of the purchased product is earlier than the next best before date in stock DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock
// Shopping list settings // Shopping list settings
DefaultUserSetting('shopping_list_to_stock_workflow_auto_submit_when_prefilled', false); // Automatically do the booking using the last price and the amount of the shopping list item, if the product has "Default best before days" set DefaultUserSetting('shopping_list_to_stock_workflow_auto_submit_when_prefilled', false); // Automatically do the booking using the last price and the amount of the shopping list item, if the product has "Default due days" set
DefaultUserSetting('shopping_list_show_calendar', false); DefaultUserSetting('shopping_list_show_calendar', false);
// Recipe settings // Recipe settings
@ -174,7 +174,7 @@ Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true);
Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true);
Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in best-before-date fields on (supported) mobile browsers Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in due date fields on (supported) mobile browsers
Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true); Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true);
Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true); Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true);

View File

@ -31,7 +31,7 @@ class StockApiController extends BaseApiController
} }
} }
public function AddExpiredProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddOverdueProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
@ -46,7 +46,7 @@ class StockApiController extends BaseApiController
$listId = intval($requestBody['list_id']); $listId = intval($requestBody['list_id']);
} }
$this->getStockService()->AddExpiredProductsToShoppingList($listId); $this->getStockService()->AddOverdueProductsToShoppingList($listId);
return $this->EmptyApiResponse($response); return $this->EmptyApiResponse($response);
} }
catch (\Exception $ex) catch (\Exception $ex)
@ -304,16 +304,18 @@ class StockApiController extends BaseApiController
{ {
$nextXDays = 5; $nextXDays = 5;
if (isset($request->getQueryParams()['expiring_days']) && !empty($request->getQueryParams()['expiring_days']) && is_numeric($request->getQueryParams()['expiring_days'])) if (isset($request->getQueryParams()['due_soon_days']) && !empty($request->getQueryParams()['due_soon_days']) && is_numeric($request->getQueryParams()['due_soon_days']))
{ {
$nextXDays = $request->getQueryParams()['expiring_days']; $nextXDays = $request->getQueryParams()['due_soon_days'];
} }
$expiringProducts = $this->getStockService()->GetExpiringProducts($nextXDays, true); $dueProducts = $this->getStockService()->GetDueProducts($nextXDays, true);
$expiredProducts = $this->getStockService()->GetExpiringProducts(-1); $overdueProducts = $this->getStockService()->GetDueProducts(-1);
$expiredProducts = $this->getStockService()->GetExpiredProducts();
$missingProducts = $this->getStockService()->GetMissingProducts(); $missingProducts = $this->getStockService()->GetMissingProducts();
return $this->ApiResponse($response, [ return $this->ApiResponse($response, [
'expiring_products' => $expiringProducts, 'due_products' => $dueProducts,
'overdue_products' => $overdueProducts,
'expired_products' => $expiredProducts, 'expired_products' => $expiredProducts,
'missing_products' => $missingProducts 'missing_products' => $missingProducts
]); ]);

View File

@ -85,7 +85,7 @@ class StockController extends BaseController
public function Overview(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Overview(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$usersService = $this->getUsersService(); $usersService = $this->getUsersService();
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expiring_soon_days']; $nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_due_soon_days'];
return $this->renderPage($response, 'stockoverview', [ return $this->renderPage($response, 'stockoverview', [
'currentStock' => $this->getStockService()->GetCurrentStockOverview(), 'currentStock' => $this->getStockService()->GetCurrentStockOverview(),
@ -425,7 +425,7 @@ class StockController extends BaseController
public function Stockentries(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Stockentries(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$usersService = $this->getUsersService(); $usersService = $this->getUsersService();
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expiring_soon_days']; $nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_due_soon_days'];
return $this->renderPage($response, 'stockentries', [ return $this->renderPage($response, 'stockentries', [
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'), 'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),

View File

@ -1258,7 +1258,7 @@
}, },
"/stock": { "/stock": {
"get": { "get": {
"summary": "Returns all products which are currently in stock incl. the next expiring date per product", "summary": "Returns all products which are currently in stock incl. the next due date per product",
"tags": [ "tags": [
"Stock" "Stock"
], ],
@ -1350,7 +1350,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The best before date of the product to add, when omitted, the current date is used" "description": "The due date of the product to add, when omitted, the current date is used"
}, },
"price": { "price": {
"type": "number", "type": "number",
@ -1416,16 +1416,16 @@
}, },
"/stock/volatile": { "/stock/volatile": {
"get": { "get": {
"summary": "Returns all products which are expiring soon, are already expired or currently missing", "summary": "Returns all products which are due soon, overdue, expired or currently missing",
"tags": [ "tags": [
"Stock" "Stock"
], ],
"parameters": [ "parameters": [
{ {
"in": "query", "in": "query",
"name": "expiring_days", "name": "due_soon_days",
"required": false, "required": false,
"description": "The number of days in which products are considered expiring soon", "description": "The number of days in which products are considered to be due soon",
"schema": { "schema": {
"type": "integer", "type": "integer",
"default": 5 "default": 5
@ -1558,7 +1558,7 @@
}, },
"/stock/products/{productId}/entries": { "/stock/products/{productId}/entries": {
"get": { "get": {
"summary": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)", "summary": "Returns all stock entries of the given product in order of next use (first due first, then first in first out)",
"tags": [ "tags": [
"Stock" "Stock"
], ],
@ -1707,7 +1707,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The best before date of the product to add, when omitted, the current date is used" "description": "The due date of the product to add, when omitted, the current date is used"
}, },
"transaction_type": { "transaction_type": {
"$ref": "#/components/internalSchemas/StockTransactionType" "$ref": "#/components/internalSchemas/StockTransactionType"
@ -1956,7 +1956,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The best before date which applies to added products" "description": "The due date which applies to added products"
}, },
"shopping_location_id": { "shopping_location_id": {
"type": "number", "type": "number",
@ -2139,7 +2139,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The best before date of the product to add, when omitted, the current date is used" "description": "The due date of the product to add, when omitted, the current date is used"
}, },
"transaction_type": { "transaction_type": {
"$ref": "#/components/internalSchemas/StockTransactionType" "$ref": "#/components/internalSchemas/StockTransactionType"
@ -2383,7 +2383,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The best before date which applies to added products" "description": "The due date which applies to added products"
}, },
"location_id": { "location_id": {
"type": "number", "type": "number",
@ -2530,9 +2530,9 @@
} }
} }
}, },
"/stock/shoppinglist/add-expired-products": { "/stock/shoppinglist/add-overdue-products": {
"post": { "post": {
"summary": "Adds expired products to the given shopping list", "summary": "Adds overdue products to the given shopping list",
"tags": [ "tags": [
"Stock" "Stock"
], ],
@ -4195,7 +4195,7 @@
"stock_amount_opened": { "stock_amount_opened": {
"type": "integer" "type": "integer"
}, },
"next_best_before_date": { "next_due_date": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
@ -4282,7 +4282,7 @@
"avg_price": null, "avg_price": null,
"oldest_price": null, "oldest_price": null,
"last_shopping_location_id": null, "last_shopping_location_id": null,
"next_best_before_date": "2019-07-07", "next_due_date": "2019-07-07",
"location": { "location": {
"id": "4", "id": "4",
"name": "Candy cupboard", "name": "Candy cupboard",
@ -4893,7 +4893,7 @@
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
"format": "date", "format": "date",
"description": "The next best before date for this product" "description": "The next due date for this product"
}, },
"is_aggregated_amount": { "is_aggregated_amount": {
"type": "boolean", "type": "boolean",
@ -4941,7 +4941,13 @@
"CurrentVolatilStockResponse": { "CurrentVolatilStockResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"expiring_products": { "due_products": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CurrentStockResponse"
}
},
"overdue_products": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/components/schemas/CurrentStockResponse" "$ref": "#/components/schemas/CurrentStockResponse"

View File

@ -20,6 +20,11 @@ msgid_plural "%s products expiring"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%s is due"
msgid_plural "%s are due"
msgstr[0] ""
msgstr[1] ""
msgid "within the next day" msgid "within the next day"
msgid_plural "within the next %s days" msgid_plural "within the next %s days"
msgstr[0] "" msgstr[0] ""
@ -30,6 +35,11 @@ msgid_plural "%s products are already expired"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%s product is overdue"
msgid_plural "%s products are overdue"
msgstr[0] ""
msgstr[1] ""
msgid "%s product is below defined min. stock amount" msgid "%s product is below defined min. stock amount"
msgid_plural "%s products are below defined min. stock amount" msgid_plural "%s products are below defined min. stock amount"
msgstr[0] "" msgstr[0] ""
@ -46,9 +56,6 @@ msgstr[1] ""
msgid "Amount" msgid "Amount"
msgstr "" msgstr ""
msgid "Next best before date"
msgstr ""
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -262,9 +269,6 @@ msgstr ""
msgid "Add products that are below defined min. stock amount" msgid "Add products that are below defined min. stock amount"
msgstr "" msgstr ""
msgid "For purchases this amount of days will be added to today for the best before date suggestion"
msgstr ""
msgid "This means 1 %1$s purchased will be converted into %2$s %3$s in stock" msgid "This means 1 %1$s purchased will be converted into %2$s %3$s in stock"
msgstr "" msgstr ""
@ -598,15 +602,9 @@ msgstr[1] ""
msgid "in singular form" msgid "in singular form"
msgstr "" msgstr ""
msgid "Never expires"
msgstr ""
msgid "This cannot be lower than %s" msgid "This cannot be lower than %s"
msgstr "" msgstr ""
msgid "-1 means that this product never expires"
msgstr ""
msgid "Quantity unit" msgid "Quantity unit"
msgstr "" msgstr ""
@ -869,18 +867,12 @@ msgstr ""
msgid "Use a specific stock item" msgid "Use a specific stock item"
msgstr "" msgstr ""
msgid "The first item in this list would be picked by the default rule which is \"First expiring first, then first in first out\"" msgid "The first item in this list would be picked by the default rule which is \"First due first, then first in first out\""
msgstr "" msgstr ""
msgid "Mark %1$s of %2$s as open" msgid "Mark %1$s of %2$s as open"
msgstr "" msgstr ""
msgid "When this product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)"
msgstr ""
msgid "Default best before days after opened"
msgstr ""
msgid "Marked %1$s of %2$s as opened" msgid "Marked %1$s of %2$s as opened"
msgstr "" msgstr ""
@ -896,7 +888,7 @@ msgstr ""
msgid "%s opened" msgid "%s opened"
msgstr "" msgstr ""
msgid "Product expires" msgid "Product due"
msgstr "" msgstr ""
msgid "Task due" msgid "Task due"
@ -1194,15 +1186,6 @@ msgstr ""
msgid "A predefined list of values, one per line" msgid "A predefined list of values, one per line"
msgstr "" msgstr ""
msgid "Chores due soon days"
msgstr ""
msgid "Batteries due to be charged soon days"
msgstr ""
msgid "Tasks due soon days"
msgstr ""
msgid "Products" msgid "Products"
msgstr "" msgstr ""
@ -1520,7 +1503,7 @@ msgstr ""
msgid "There are no units available at this location" msgid "There are no units available at this location"
msgstr "" msgstr ""
msgid "Amount: %1$s; Expires on %2$s; Bought on %3$s" msgid "Amount: %1$s; Due on %2$s; Bought on %3$s"
msgstr "" msgstr ""
msgid "Transfered %1$s of %2$s from %3$s to %4$s" msgid "Transfered %1$s of %2$s from %3$s to %4$s"
@ -1625,18 +1608,6 @@ msgstr ""
msgid "When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings" msgid "When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings"
msgstr "" msgstr ""
msgid "On moving this product to a freezer location (so when freezing it), 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 (so when thawing it), 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" msgid "This cannot be the same as the \"From\" location"
msgstr "" msgstr ""
@ -1757,7 +1728,7 @@ msgstr ""
msgid "Not enough in stock (not included in costs), %s ingredient missing" msgid "Not enough in stock (not included in costs), %s ingredient missing"
msgstr "" msgstr ""
msgid "Based on the prices of the default consume rule which is \"First expiring first, then first in first out\"" msgid "Based on the prices of the default consume rule which is \"First due first, then first in first out\""
msgstr "" msgstr ""
msgid "Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list" msgid "Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list"
@ -1928,10 +1899,10 @@ msgstr ""
msgid "This is the default quantity unit used when adding this product to the shopping list" msgid "This is the default quantity unit used when adding this product to the shopping list"
msgstr "" msgstr ""
msgid "Show a warning when the best before date of the purchased product is earlier than the next best before date in stock" msgid "Show a warning when the due date of the purchased product is earlier than the next due date in stock"
msgstr "" msgstr ""
msgid "There are items in stock which expire earlier" msgid "There are items in stock which are due earlier"
msgstr "" msgstr ""
msgid "When enabled, the amount will always be filled with 1 after changing/scanning a product and if all fields could be automatically populated (by product defaults), the transaction is automatically submitted" msgid "When enabled, the amount will always be filled with 1 after changing/scanning a product and if all fields could be automatically populated (by product defaults), the transaction is automatically submitted"
@ -1948,3 +1919,70 @@ msgstr ""
msgid "Are you sure to remove this barcode?" msgid "Are you sure to remove this barcode?"
msgstr "" msgstr ""
msgid "Due date type"
msgstr ""
msgid "Based on the selected type, the highlighting on the stock overview page will be different"
msgstr ""
msgid "Means that the product is maybe still safe to be consumed after its due date is reached"
msgstr ""
msgid "Expiration date"
msgstr ""
msgid "Means that the product is not safe to be consumed after its due date is reached"
msgstr ""
msgid "For purchases this amount of days will be added to today for the due date suggestion"
msgstr ""
msgid "-1 means that this product wille be never overdue"
msgstr ""
msgid "Default due days"
msgstr ""
msgid "When this product was marked as opened, the expiry date will be replaced by today + this amount of days (a value of 0 disables this)"
msgstr ""
msgid "Default due days after opened"
msgstr ""
msgid "On moving this product to a freezer location (so when freezing it), the expiry date will be replaced by today + this amount of days"
msgstr ""
msgid "Default due days after freezing"
msgstr ""
msgid "On moving this product from a freezer location (so when thawing it), the due date will be replaced by today + this amount of days"
msgstr ""
msgid "Default due days after thawing"
msgstr ""
msgid "Next due date"
msgstr ""
msgid "%s product is due"
msgid_plural "%s products are due"
msgstr[0] ""
msgstr[1] ""
msgid "Due date"
msgstr ""
msgid "Never overdue"
msgstr ""
msgid "%s product is expired"
msgid_plural "%s products are expired"
msgstr[0] ""
msgstr[1] ""
msgid "Expired"
msgstr ""
msgid "Due soon days"
msgstr ""

View File

@ -68,6 +68,7 @@ CREATE TABLE products (
parent_product_id INT, parent_product_id INT,
calories INTEGER, calories INTEGER,
cumulate_min_stock_amount_of_sub_products TINYINT DEFAULT 0, cumulate_min_stock_amount_of_sub_products TINYINT DEFAULT 0,
due_type TINYINT NOT NULL DEFAULT 1 CHECK(due_type IN (1, 2)),
quick_consume_amount REAL NOT NULL DEFAULT 1, quick_consume_amount REAL NOT NULL DEFAULT 1,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
); );

View File

@ -23,15 +23,18 @@ SELECT
p.calories AS product_calories, p.calories AS product_calories,
sc.amount * p.calories AS calories, sc.amount * p.calories AS calories,
sc.amount_aggregated * p.calories AS calories_aggregated, sc.amount_aggregated * p.calories AS calories_aggregated,
p.quick_consume_amount p.quick_consume_amount,
p.due_type
FROM ( FROM (
SELECT * SELECT *
FROM stock_current FROM stock_current
WHERE best_before_date IS NOT NULL WHERE best_before_date IS NOT NULL
UNION UNION
SELECT id, 0, 0, 0, null, 0, 0, 0 SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type
FROM stock_missing_products_including_opened FROM stock_missing_products_including_opened m
WHERE id NOT IN (SELECT product_id FROM stock_current) JOIN products p
ON m.id = p.id
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc ) sc
LEFT JOIN products p LEFT JOIN products p
ON sc.product_id = p.id; ON sc.product_id = p.id;
@ -61,15 +64,18 @@ SELECT
p.calories AS product_calories, p.calories AS product_calories,
sc.amount * p.calories AS calories, sc.amount * p.calories AS calories,
sc.amount_aggregated * p.calories AS calories_aggregated, sc.amount_aggregated * p.calories AS calories_aggregated,
p.quick_consume_amount p.quick_consume_amount,
p.due_type
FROM ( FROM (
SELECT * SELECT *
FROM stock_current FROM stock_current
WHERE best_before_date IS NOT NULL WHERE best_before_date IS NOT NULL
UNION UNION
SELECT id, 0, 0, 0, null, 0, 0, 0 SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type
FROM stock_missing_products FROM stock_missing_products m
WHERE id NOT IN (SELECT product_id FROM stock_current) JOIN products p
ON m.id = p.id
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc ) sc
LEFT JOIN products p LEFT JOIN products p
ON sc.product_id = p.id; ON sc.product_id = p.id;

View File

@ -9,7 +9,8 @@ SELECT
MIN(s.best_before_date) AS best_before_date, MIN(s.best_before_date) AS best_before_date,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id AND open = 1), 0) AS amount_opened, IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id AND open = 1), 0) AS amount_opened,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = pr.parent_product_id) AND open = 1), 0) * IFNULL(qucr.factor, 1) AS amount_opened_aggregated, IFNULL((SELECT SUM(amount) FROM stock WHERE product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = pr.parent_product_id) AND open = 1), 0) * IFNULL(qucr.factor, 1) AS amount_opened_aggregated,
CASE WHEN COUNT(p_sub.parent_product_id) > 0 THEN 1 ELSE 0 END AS is_aggregated_amount CASE WHEN COUNT(p_sub.parent_product_id) > 0 THEN 1 ELSE 0 END AS is_aggregated_amount,
MAX(p_parent.due_type) AS due_type
FROM products_resolved pr FROM products_resolved pr
JOIN stock s JOIN stock s
ON pr.sub_product_id = s.product_id ON pr.sub_product_id = s.product_id
@ -37,10 +38,14 @@ SELECT
MIN(s.best_before_date) AS best_before_date, MIN(s.best_before_date) AS best_before_date,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened, IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened_aggregated, IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened_aggregated,
0 AS is_aggregated_amount 0 AS is_aggregated_amount,
MAX(p_sub.due_type) AS due_type
FROM products_resolved pr FROM products_resolved pr
JOIN stock s JOIN stock s
ON pr.sub_product_id = s.product_id ON pr.sub_product_id = s.product_id
JOIN products p_sub
ON pr.sub_product_id = p_sub.id
AND p_sub.active = 1
WHERE pr.parent_product_id != pr.sub_product_id WHERE pr.parent_product_id != pr.sub_product_id
GROUP BY pr.sub_product_id GROUP BY pr.sub_product_id
HAVING SUM(s.amount) > 0; HAVING SUM(s.amount) > 0;

View File

@ -1 +1 @@
update user_settings set key = "stock_expiring_soon_days" where key = "stock_expring_soon_days"; update user_settings set key = "stock_due_soon_days" where key = "stock_expring_soon_days";

View File

@ -460,7 +460,8 @@ canvas.drawingBuffer {
.warning-message, .warning-message,
.error-message, .error-message,
.normal-message { .normal-message,
.secondary-message {
padding: 12px; padding: 12px;
font-weight: bold; font-weight: bold;
width: fit-content; width: fit-content;
@ -485,6 +486,12 @@ canvas.drawingBuffer {
border-top-color: #4c63b6; border-top-color: #4c63b6;
} }
.secondary-message {
background-color: #e1e4e8;
color: #4e575f;
border-top-color: #68696b;
}
.status-filter-message, .status-filter-message,
.user-filter-message { .user-filter-message {
display: inline-block; display: inline-block;

View File

@ -219,7 +219,7 @@ $("#location_id").on('change', function(e)
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
text: __t("Amount: %1$s; Expires on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
})); }));
sumValue = sumValue + parseFloat(stockEntry.amount); sumValue = sumValue + parseFloat(stockEntry.amount);

View File

@ -99,11 +99,11 @@ $('#save-purchase-button').on('click', function(e)
toastr.success(successMessage); toastr.success(successMessage);
Grocy.Components.ProductPicker.FinishFlow(); Grocy.Components.ProductPicker.FinishFlow();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_best_before_date_is_earlier_than_next)) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next))
{ {
if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_best_before_date)) if (moment(jsonData.best_before_date).isBefore(CurrentProductDetails.next_due_date))
{ {
toastr.warning(__t("There are items in stock which expire earlier")); toastr.warning(__t("There are items in stock which are due earlier"));
} }
} }

View File

@ -174,9 +174,9 @@ $(document).on('click', '#add-products-below-min-stock-amount', function(e)
); );
}); });
$(document).on('click', '#add-expired-products', function(e) $(document).on('click', '#add-overdue-products', function(e)
{ {
Grocy.Api.Post('stock/shoppinglist/add-expired-products', { "list_id": $("#selected-shopping-list").val() }, Grocy.Api.Post('stock/shoppinglist/add-overdue-products', { "list_id": $("#selected-shopping-list").val() },
function(result) function(result)
{ {
window.location.href = U('/shoppinglist?list=' + $("#selected-shopping-list").val()); window.location.href = U('/shoppinglist?list=' + $("#selected-shopping-list").val());

View File

@ -148,7 +148,7 @@ function RefreshStockEntryRow(stockRowId)
} }
else else
{ {
var expiringThreshold = moment().add(Grocy.UserSettings.stock_expiring_soon_days, "days"); var dueThreshold = moment().add(Grocy.UserSettings.stock_due_soon_days, "days");
var now = moment(); var now = moment();
var bestBeforeDate = moment(result.best_before_date); var bestBeforeDate = moment(result.best_before_date);
@ -158,10 +158,17 @@ function RefreshStockEntryRow(stockRowId)
stockRow.removeClass("d-none"); stockRow.removeClass("d-none");
stockRow.removeAttr("style"); stockRow.removeAttr("style");
if (now.isAfter(bestBeforeDate)) if (now.isAfter(bestBeforeDate))
{
if (stockRow.attr("data-due-type") == 1)
{
stockRow.addClass("table-secondary");
}
else
{ {
stockRow.addClass("table-danger"); stockRow.addClass("table-danger");
} }
else if (bestBeforeDate.isBefore(expiringThreshold)) }
else if (bestBeforeDate.isBefore(dueThreshold))
{ {
stockRow.addClass("table-warning"); stockRow.addClass("table-warning");
} }
@ -169,8 +176,8 @@ function RefreshStockEntryRow(stockRowId)
animateCSS("#stock-" + stockRowId + "-row td:not(:first)", "shake"); animateCSS("#stock-" + stockRowId + "-row td:not(:first)", "shake");
$('#stock-' + stockRowId + '-amount').text(result.amount); $('#stock-' + stockRowId + '-amount').text(result.amount);
$('#stock-' + stockRowId + '-best-before-date').text(result.best_before_date); $('#stock-' + stockRowId + '-due-date').text(result.best_before_date);
$('#stock-' + stockRowId + '-best-before-date-timeago').attr('datetime', result.best_before_date + ' 23:59:59'); $('#stock-' + stockRowId + '-due-date-timeago').attr('datetime', result.best_before_date + ' 23:59:59');
$(".stock-consume-button").attr('data-location-id', result.location_id); $(".stock-consume-button").attr('data-location-id', result.location_id);

View File

@ -233,12 +233,13 @@ function RefreshStatistics()
} }
); );
var nextXDays = $("#info-expiring-products").data("next-x-days"); var nextXDays = $("#info-duesoon-products").data("next-x-days");
Grocy.Api.Get('stock/volatile?expiring_days=' + nextXDays, Grocy.Api.Get('stock/volatile?due_soon_days=' + nextXDays,
function(result) function(result)
{ {
$("#info-expiring-products").html('<span class="d-block d-md-none">' + result.expiring_products.length + ' <i class="fas fa-clock"></i></span><span class="d-none d-md-block">' + __n(result.expiring_products.length, '%s product expires', '%s products expiring') + ' ' + __n(nextXDays, 'within the next day', 'within the next %s days') + '</span>'); $("#info-duesoon-products").html('<span class="d-block d-md-none">' + result.due_products.length + ' <i class="fas fa-clock"></i></span><span class="d-none d-md-block">' + __n(result.due_products.length, '%s product is due', '%s products are due') + ' ' + __n(nextXDays, 'within the next day', 'within the next %s days') + '</span>');
$("#info-expired-products").html('<span class="d-block d-md-none">' + result.expired_products.length + ' <i class="fas fa-times-circle"></i></span><span class="d-none d-md-block">' + __n(result.expired_products.length, '%s product is already expired', '%s products are already expired') + '</span>'); $("#info-overdue-products").html('<span class="d-block d-md-none">' + result.overdue_products.length + ' <i class="fas fa-times-circle"></i></span><span class="d-none d-md-block">' + __n(result.overdue_products.length, '%s product is overdue', '%s products are overdue') + '</span>');
$("#info-expired-products").html('<span class="d-block d-md-none">' + result.expired_products.length + ' <i class="fas fa-times-circle"></i></span><span class="d-none d-md-block">' + __n(result.expired_products.length, '%s product is expired', '%s products are expired') + '</span>');
$("#info-missing-products").html('<span class="d-block d-md-none">' + result.missing_products.length + ' <i class="fas fa-exclamation-circle"></i></span><span class="d-none d-md-block">' + __n(result.missing_products.length, '%s product is below defined min. stock amount', '%s products are below defined min. stock amount') + '</span>'); $("#info-missing-products").html('<span class="d-block d-md-none">' + result.missing_products.length + ' <i class="fas fa-exclamation-circle"></i></span><span class="d-none d-md-block">' + __n(result.missing_products.length, '%s product is below defined min. stock amount', '%s products are below defined min. stock amount') + '</span>');
}, },
function(xhr) function(xhr)
@ -263,20 +264,28 @@ function RefreshProductRow(productId)
} }
var productRow = $('#product-' + productId + '-row'); var productRow = $('#product-' + productId + '-row');
var expiringThreshold = moment().add($("#info-expiring-products").data("next-x-days"), "days"); var dueSoonThreshold = moment().add($("#info-duesoon-products").data("next-x-days"), "days");
var now = moment(); var now = moment();
var nextBestBeforeDate = moment(result.next_best_before_date); var nextDueDate = moment(result.next_due_date);
productRow.removeClass("table-warning"); productRow.removeClass("table-warning");
productRow.removeClass("table-danger"); productRow.removeClass("table-danger");
productRow.removeClass("table-secondary");
productRow.removeClass("table-info"); productRow.removeClass("table-info");
productRow.removeClass("d-none"); productRow.removeClass("d-none");
productRow.removeAttr("style"); productRow.removeAttr("style");
if (now.isAfter(nextBestBeforeDate)) if (now.isAfter(nextDueDate))
{
if (result.product.due_type == 1)
{
productRow.addClass("table-secondary");
}
else
{ {
productRow.addClass("table-danger"); productRow.addClass("table-danger");
} }
else if (nextBestBeforeDate.isBefore(expiringThreshold)) }
else if (nextDueDate.isBefore(dueSoonThreshold))
{ {
productRow.addClass("table-warning"); productRow.addClass("table-warning");
} }
@ -297,8 +306,8 @@ function RefreshProductRow(productId)
$('#product-' + productId + '-amount').text(result.stock_amount); $('#product-' + productId + '-amount').text(result.stock_amount);
$('#product-' + productId + '-consume-all-button').attr('data-consume-amount', result.stock_amount); $('#product-' + productId + '-consume-all-button').attr('data-consume-amount', result.stock_amount);
$('#product-' + productId + '-value').text(result.stock_value); $('#product-' + productId + '-value').text(result.stock_value);
$('#product-' + productId + '-next-best-before-date').text(result.next_best_before_date); $('#product-' + productId + '-next-due-date').text(result.next_due_date);
$('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); $('#product-' + productId + '-next-due-date-timeago').attr('datetime', result.next_due_date);
var openedAmount = result.stock_amount_opened || 0; var openedAmount = result.stock_amount_opened || 0;
if (openedAmount > 0) if (openedAmount > 0)
@ -316,8 +325,8 @@ function RefreshProductRow(productId)
} }
} }
$('#product-' + productId + '-next-best-before-date').text(result.next_best_before_date); $('#product-' + productId + '-next-due-date').text(result.next_due_date);
$('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date + ' 23:59:59'); $('#product-' + productId + '-next-due-date-timeago').attr('datetime', result.next_due_date + ' 23:59:59');
if (result.stock_amount_opened > 0) if (result.stock_amount_opened > 0)
{ {

View File

@ -1,7 +1,7 @@
$("#product_presets_location_id").val(Grocy.UserSettings.product_presets_location_id); $("#product_presets_location_id").val(Grocy.UserSettings.product_presets_location_id);
$("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id); $("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
$("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id); $("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id);
$("#stock_expiring_soon_days").val(Grocy.UserSettings.stock_expiring_soon_days); $("#stock_due_soon_days").val(Grocy.UserSettings.stock_due_soon_days);
$("#stock_default_purchase_amount").val(Grocy.UserSettings.stock_default_purchase_amount); $("#stock_default_purchase_amount").val(Grocy.UserSettings.stock_default_purchase_amount);
$("#stock_default_consume_amount").val(Grocy.UserSettings.stock_default_consume_amount); $("#stock_default_consume_amount").val(Grocy.UserSettings.stock_default_consume_amount);
$("#stock_decimal_places_amounts").val(Grocy.UserSettings.stock_decimal_places_amounts); $("#stock_decimal_places_amounts").val(Grocy.UserSettings.stock_decimal_places_amounts);
@ -16,9 +16,9 @@ if (BoolVal(Grocy.UserSettings.show_purchased_date_on_purchase))
$("#show_purchased_date_on_purchase").prop("checked", true); $("#show_purchased_date_on_purchase").prop("checked", true);
} }
if (BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_best_before_date_is_earlier_than_next)) if (BoolVal(Grocy.UserSettings.show_warning_on_purchase_when_due_date_is_earlier_than_next))
{ {
$("#show_warning_on_purchase_when_best_before_date_is_earlier_than_next").prop("checked", true); $("#show_warning_on_purchase_when_due_date_is_earlier_than_next").prop("checked", true);
} }
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();

View File

@ -278,7 +278,7 @@ $("#location_id_from").on('change', function(e)
$("#specific_stock_entry").append($("<option>", { $("#specific_stock_entry").append($("<option>", {
value: stockEntry.stock_id, value: stockEntry.stock_id,
amount: stockEntry.amount, amount: stockEntry.amount,
text: __t("Amount: %1$s; Expires on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt text: __t("Amount: %1$s; Due on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
})); }));
if (stockEntry.stock_id == stockId) if (stockEntry.stock_id == stockId)
{ {

View File

@ -210,7 +210,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
if (GROCY_FEATURE_FLAG_SHOPPINGLIST) if (GROCY_FEATURE_FLAG_SHOPPINGLIST)
{ {
$group->post('/stock/shoppinglist/add-missing-products', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList'); $group->post('/stock/shoppinglist/add-missing-products', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
$group->post('/stock/shoppinglist/add-expired-products', '\Grocy\Controllers\StockApiController:AddExpiredProductsToShoppingList'); $group->post('/stock/shoppinglist/add-overdue-products', '\Grocy\Controllers\StockApiController:AddOverdueProductsToShoppingList');
$group->post('/stock/shoppinglist/clear', '\Grocy\Controllers\StockApiController:ClearShoppingList'); $group->post('/stock/shoppinglist/clear', '\Grocy\Controllers\StockApiController:ClearShoppingList');
$group->post('/stock/shoppinglist/add-product', '\Grocy\Controllers\StockApiController:AddProductToShoppingList'); $group->post('/stock/shoppinglist/add-product', '\Grocy\Controllers\StockApiController:AddProductToShoppingList');
$group->post('/stock/shoppinglist/remove-product', '\Grocy\Controllers\StockApiController:RemoveProductFromShoppingList'); $group->post('/stock/shoppinglist/remove-product', '\Grocy\Controllers\StockApiController:RemoveProductFromShoppingList');

View File

@ -13,7 +13,7 @@ class CalendarService extends BaseService
if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{ {
$products = $this->getDatabase()->products(); $products = $this->getDatabase()->products();
$titlePrefix = $this->getLocalizationService()->__t('Product expires') . ': '; $titlePrefix = $this->getLocalizationService()->__t('Product due') . ': ';
foreach ($this->getStockService()->GetCurrentStock() as $currentStockEntry) foreach ($this->getStockService()->GetCurrentStock() as $currentStockEntry)
{ {

View File

@ -85,7 +85,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('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('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('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, 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, default_best_before_days_after_freezing, default_best_before_days_after_thawing, due_type) VALUES ('{$this->__t_sql('Minced meat')}', 2, 3, 3, 1, 4, 180, 2, 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, 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('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 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
@ -250,6 +250,7 @@ class DemoDataGeneratorService extends BaseService
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(20, 1, date('Y-m-d', strtotime('-1 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId);

View File

@ -52,25 +52,24 @@ class StockService extends BaseService
} }
} }
public function AddExpiredProductsToShoppingList($listId = 1) public function AddOverdueProductsToShoppingList($listId = 1)
{ {
if (!$this->ShoppingListExists($listId)) if (!$this->ShoppingListExists($listId))
{ {
throw new \Exception('Shopping list does not exist'); throw new \Exception('Shopping list does not exist');
} }
$expiredProducts = $this->GetExpiringProducts(-1); $overdueProducts = $this->GetDueProducts(-1);
foreach ($expiredProducts as $expiredProduct) foreach ($overdueProducts as $overdueProduct)
{ {
$product = $this->getDatabase()->products()->where('id', $expiredProduct->product_id)->fetch(); $product = $this->getDatabase()->products()->where('id', $overdueProduct->product_id)->fetch();
$alreadyExistingEntry = $this->getDatabase()->shopping_list()->where('product_id', $expiredProduct->product_id)->fetch();
$alreadyExistingEntry = $this->getDatabase()->shopping_list()->where('product_id', $overdueProduct->product_id)->fetch();
if (!$alreadyExistingEntry) if (!$alreadyExistingEntry)
{ {
$shoppinglistRow = $this->getDatabase()->shopping_list()->createRow([ $shoppinglistRow = $this->getDatabase()->shopping_list()->createRow([
'product_id' => $expiredProduct->product_id, 'product_id' => $overdueProduct->product_id,
'amount' => 1, 'amount' => 1,
'shopping_list_id' => $listId 'shopping_list_id' => $listId
]); ]);
@ -101,7 +100,7 @@ class StockService extends BaseService
$amount = $amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight); $amount = $amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight);
} }
//Sets the default best before date, if none is supplied //Set the default due date, if none is supplied
if ($bestBeforeDate == null) if ($bestBeforeDate == null)
{ {
if (intval($productDetails->product->default_best_before_days) == -1) if (intval($productDetails->product->default_best_before_days) == -1)
@ -479,12 +478,13 @@ class StockService extends BaseService
} }
} }
public function GetExpiringProducts(int $days = 5, bool $excludeExpired = false) public function GetDueProducts(int $days = 5, bool $excludeOverdue = false)
{ {
$currentStock = $this->GetCurrentStock(false); $currentStock = $this->GetCurrentStock(false);
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<'); $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<');
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'due_type', 1);
if ($excludeExpired) if ($excludeOverdue)
{ {
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime('-1 days')), '>'); $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime('-1 days')), '>');
} }
@ -492,6 +492,15 @@ class StockService extends BaseService
return $currentStock; return $currentStock;
} }
public function GetExpiredProducts()
{
$currentStock = $this->GetCurrentStock(false);
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("-1 days")), '<');
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'due_type', 2);
return $currentStock;
}
public function GetMissingProducts() public function GetMissingProducts()
{ {
$sql = 'SELECT * FROM stock_missing_products_including_opened'; $sql = 'SELECT * FROM stock_missing_products_including_opened';
@ -550,7 +559,7 @@ class StockService extends BaseService
$product = $this->getDatabase()->products($productId); $product = $this->getDatabase()->products($productId);
$productBarcodes = $this->getDatabase()->product_barcodes()->where('product_id', $productId)->fetchAll(); $productBarcodes = $this->getDatabase()->product_barcodes()->where('product_id', $productId)->fetchAll();
$productLastUsed = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date'); $productLastUsed = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date');
$nextBestBeforeDate = $this->getDatabase()->stock()->where('product_id', $productId)->min('best_before_date'); $nextDueDate = $this->getDatabase()->stock()->where('product_id', $productId)->min('best_before_date');
$quPurchase = $this->getDatabase()->quantity_units($product->qu_id_purchase); $quPurchase = $this->getDatabase()->quantity_units($product->qu_id_purchase);
$quStock = $this->getDatabase()->quantity_units($product->qu_id_stock); $quStock = $this->getDatabase()->quantity_units($product->qu_id_stock);
$location = $this->getDatabase()->locations($product->location_id); $location = $this->getDatabase()->locations($product->location_id);
@ -584,7 +593,7 @@ class StockService extends BaseService
'oldest_price' => $oldestPrice, 'oldest_price' => $oldestPrice,
'last_shopping_location_id' => $lastShoppingLocation, 'last_shopping_location_id' => $lastShoppingLocation,
'default_shopping_location_id' => $product->shopping_location_id, 'default_shopping_location_id' => $product->shopping_location_id,
'next_best_before_date' => $nextBestBeforeDate, 'next_due_date' => $nextDueDate,
'location' => $location, 'location' => $location,
'average_shelf_life_days' => $averageShelfLifeDays, 'average_shelf_life_days' => $averageShelfLifeDays,
'spoil_rate_percent' => $spoilRate, 'spoil_rate_percent' => $spoilRate,
@ -630,7 +639,7 @@ class StockService extends BaseService
public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false, $ordered = true) public function GetProductStockEntries($productId, $excludeOpened = false, $allowSubproductSubstitution = false, $ordered = true)
{ {
// In order of next use: // In order of next use:
// First expiring first, then first in first out // First due first, then first in first out
$sqlWhereProductId = 'product_id = :1'; $sqlWhereProductId = 'product_id = :1';

View File

@ -19,7 +19,7 @@
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'batteries_due_soon_days', 'id' => 'batteries_due_soon_days',
'additionalAttributes' => 'data-setting-key="batteries_due_soon_days"', 'additionalAttributes' => 'data-setting-key="batteries_due_soon_days"',
'label' => 'Batteries due to be charged soon days', 'label' => 'Due soon days',
'min' => 1, 'min' => 1,
'invalidFeedback' => $__t('This cannot be lower than %s', '1'), 'invalidFeedback' => $__t('This cannot be lower than %s', '1'),
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control'

View File

@ -19,7 +19,7 @@
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'chores_due_soon_days', 'id' => 'chores_due_soon_days',
'additionalAttributes' => 'data-setting-key="chores_due_soon_days"', 'additionalAttributes' => 'data-setting-key="chores_due_soon_days"',
'label' => 'Chores due soon days', 'label' => 'Due soon days',
'min' => 1, 'min' => 1,
'invalidFeedback' => $__t('This cannot be lower than %s', '1'), 'invalidFeedback' => $__t('This cannot be lower than %s', '1'),
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control'

View File

@ -118,7 +118,7 @@
for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }} for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"') }}"></i> title="{{ $__t('The first item in this list would be picked by the default rule which is "First due first, then first in first out"') }}"></i>
</label> </label>
</div> </div>
<select disabled <select disabled

View File

@ -60,17 +60,17 @@
@endphp @endphp
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'label' => 'Best before', 'label' => 'Due date',
'hint' => 'This will apply to added products', 'hint' => 'This will apply to added products',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
'initWithNow' => false, 'initWithNow' => false,
'limitEndToNow' => false, 'limitEndToNow' => false,
'limitStartToNow' => false, 'limitStartToNow' => false,
'invalidFeedback' => $__t('A best before date is required'), 'invalidFeedback' => $__t('A due date is required'),
'nextInputSelector' => '#best_before_date', 'nextInputSelector' => '#best_before_date',
'additionalGroupCssClasses' => 'date-only-datetimepicker', 'additionalGroupCssClasses' => 'date-only-datetimepicker',
'shortcutValue' => '2999-12-31', 'shortcutValue' => '2999-12-31',
'shortcutLabel' => 'Never expires', 'shortcutLabel' => 'Never overdue',
'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, 'additionalGroupCssClasses' => $additionalGroupCssClasses,

View File

@ -61,7 +61,7 @@
for="is_freezer">{{ $__t('Is freezer') }} for="is_freezer">{{ $__t('Is freezer') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('When moving products from/to a freezer location, the products best before date is automatically adjusted according to the product settings') }}"></i> title="{{ $__t('When moving products from/to a freezer location, the products due date is automatically adjusted according to the product settings') }}"></i>
</label> </label>
</div> </div>
</div> </div>

View File

@ -159,31 +159,72 @@
for="cumulate_min_stock_amount_of_sub_products">{{ $__t('Accumulate sub products min. stock amount') }} for="cumulate_min_stock_amount_of_sub_products">{{ $__t('Accumulate sub products min. stock amount') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('If enabled, the min. stock amount of sub products will be accumulated into this product, means the sub product will never be "missing", only this product') }}"></i></span> title="{{ $__t('If enabled, the min. stock amount of sub products will be accumulated into this product, means the sub product will never be "missing", only this product') }}"></i>
</label> </label>
</div> </div>
</div> </div>
@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<div class="form-group">
<label class="d-block my-0"
for="location_id">{{ $__t('Due date type') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
title="{{ $__t('Based on the selected type, the highlighting on the stock overview page will be different') }}"></i>
</label>
<div class="custom-control custom-radio mt-n2">
<input class="custom-control-input"
type="radio"
name="due_type"
id="due-type-bestbefore"
value="1"
@if($mode=='edit'
&&
$product->due_type == 1) checked @else checked @endif>
<label class="custom-control-label"
for="due-type-bestbefore">{{ $__t('Best before date') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
title="{{ $__t('Means that the product is maybe still safe to be consumed after its due date is reached') }}"></i>
</label>
</div>
<div class="custom-control custom-radio mt-n2">
<input class="custom-control-input"
type="radio"
name="due_type"
id="due-type-expiration"
value="2"
@if($mode=='edit'
&&
$product->due_type == 2) checked @endif>
<label class="custom-control-label"
for="due-type-expiration">{{ $__t('Expiration date') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
title="{{ $__t('Means that the product is not safe to be consumed after its due date is reached') }}"></i>
</label>
</div>
</div>
@php if($mode == 'edit') { $value = $product->default_best_before_days; } else { $value = 0; } @endphp @php if($mode == 'edit') { $value = $product->default_best_before_days; } else { $value = 0; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'default_best_before_days', 'id' => 'default_best_before_days',
'label' => 'Default best before days', 'label' => 'Default due days',
'min' => -1, 'min' => -1,
'value' => $value, 'value' => $value,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '-1'), 'invalidFeedback' => $__t('The amount cannot be lower than %s', '-1'),
'hint' => $__t('For purchases this amount of days will be added to today for the best before date suggestion') . ' (' . $__t('-1 means that this product never expires') . ')' 'hint' => $__t('For purchases this amount of days will be added to today for the due date suggestion') . ' (' . $__t('-1 means that this product wille be never overdue') . ')'
)) ))
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_open; } else { $value = 0; } @endphp @php if($mode == 'edit') { $value = $product->default_best_before_days_after_open; } else { $value = 0; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'default_best_before_days_after_open', 'id' => 'default_best_before_days_after_open',
'label' => 'Default best before days after opened', 'label' => 'Default due days after opened',
'min' => 0, 'min' => 0,
'value' => $value, 'value' => $value,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '-1'), 'invalidFeedback' => $__t('The amount cannot be lower than %s', '-1'),
'hint' => $__t('When this product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)') 'hint' => $__t('When this product was marked as opened, the expiry date will be replaced by today + this amount of days (a value of 0 disables this)')
)) ))
@else @else
<input type="hidden" <input type="hidden"
@ -196,6 +237,10 @@
name="default_best_before_days" name="default_best_before_days"
id="default_best_before_days" id="default_best_before_days"
value="1"> value="1">
<input type="hidden"
name="due_type"
id="due_type"
value="1">
@endif @endif
<div class="form-group"> <div class="form-group">
@ -344,21 +389,21 @@
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_freezing; } else { $value = 0; } @endphp @php if($mode == 'edit') { $value = $product->default_best_before_days_after_freezing; } else { $value = 0; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'default_best_before_days_after_freezing', 'id' => 'default_best_before_days_after_freezing',
'label' => 'Default best before days after freezing', 'label' => 'Default due days after freezing',
'min' => -1, 'min' => -1,
'value' => $value, 'value' => $value,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'),
'hint' => $__t('On moving this product to a freezer location (so when freezing it), the best before date will be replaced by today + this amount of days') 'hint' => $__t('On moving this product to a freezer location (so when freezing it), the expiry 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 @php if($mode == 'edit') { $value = $product->default_best_before_days_after_thawing; } else { $value = 0; } @endphp
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'default_best_before_days_after_thawing', 'id' => 'default_best_before_days_after_thawing',
'label' => 'Default best before days after thawing', 'label' => 'Default due days after thawing',
'min' => -1, 'min' => -1,
'value' => $value, 'value' => $value,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'), 'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'),
'hint' => $__t('On moving this product from a freezer location (so when thawing it), the best before date will be replaced by today + this amount of days') 'hint' => $__t('On moving this product from a freezer location (so when thawing it), the due date will be replaced by today + this amount of days')
)) ))
@else @else
<input type="hidden" <input type="hidden"

View File

@ -81,16 +81,16 @@
@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'label' => 'Best before', 'label' => 'Due date',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
'initWithNow' => false, 'initWithNow' => false,
'limitEndToNow' => false, 'limitEndToNow' => false,
'limitStartToNow' => false, 'limitStartToNow' => false,
'invalidFeedback' => $__t('A best before date is required'), 'invalidFeedback' => $__t('A due date is required'),
'nextInputSelector' => '#price', 'nextInputSelector' => '#price',
'additionalCssClasses' => 'date-only-datetimepicker', 'additionalCssClasses' => 'date-only-datetimepicker',
'shortcutValue' => '2999-12-31', 'shortcutValue' => '2999-12-31',
'shortcutLabel' => 'Never expires', 'shortcutLabel' => 'Never overdue',
'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?'),
'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD
@ -111,23 +111,23 @@
'additionalCssClasses' => 'locale-number-input locale-number-currency' 'additionalCssClasses' => 'locale-number-input locale-number-currency'
)) ))
<div class="form-check form-check-inline mb-3"> <div class="custom-control custom-radio custom-control-inline mt-n2 mb-3">
<input class="form-check-input" <input class="custom-control-input"
type="radio" type="radio"
name="price-type" name="price-type"
id="price-type-unit-price" id="price-type-unit-price"
value="unit-price" value="unit-price"
checked> checked>
<label class="form-check-label" <label class="custom-control-label"
for="price-type-unit-price">{{ $__t('Unit price') }}</label> for="price-type-unit-price">{{ $__t('Unit price') }}</label>
</div> </div>
<div class="form-check form-check-inline mb-3"> <div class="custom-control custom-radio custom-control-inline mt-n2 mb-3">
<input class="form-check-input" <input class="custom-control-input"
type="radio" type="radio"
name="price-type" name="price-type"
id="price-type-total-price" id="price-type-total-price"
value="total-price"> value="total-price">
<label class="form-check-label" <label class="custom-control-label"
for="price-type-total-price">{{ $__t('Total price') }}</label> for="price-type-total-price">{{ $__t('Total price') }}</label>
</div> </div>
@include('components.shoppinglocationpicker', array( @include('components.shoppinglocationpicker', array(

View File

@ -320,7 +320,7 @@
<label>{{ $__t('Costs') }}&nbsp;</label> <label>{{ $__t('Costs') }}&nbsp;</label>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Based on the prices of the default consume rule which is "First expiring first, then first in first out"') }}"></i> title="{{ $__t('Based on the prices of the default consume rule which is "First due first, then first in first out"') }}"></i>
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3> <h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3>
</div> </div>
@endif @endif

View File

@ -115,10 +115,10 @@
href="#"> href="#">
{{ $__t('Add products that are below defined min. stock amount') }} {{ $__t('Add products that are below defined min. stock amount') }}
</a> </a>
<a id="add-expired-products" <a id="add-overdue-products"
class="btn btn-outline-primary btn-sm mb-1 responsive-button @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif" class="btn btn-outline-primary btn-sm mb-1 responsive-button @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif"
href="#"> href="#">
{{ $__t('Add expired products') }} {{ $__t('Add overdue products') }}
</a> </a>
</div> </div>
</div> </div>

View File

@ -38,7 +38,7 @@
data-setting-key="shopping_list_to_stock_workflow_auto_submit_when_prefilled"> data-setting-key="shopping_list_to_stock_workflow_auto_submit_when_prefilled">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="shopping_list_to_stock_workflow_auto_submit_when_prefilled"> for="shopping_list_to_stock_workflow_auto_submit_when_prefilled">
{{ $__t('Automatically do the booking using the last price and the amount of the shopping list item, if the product has "Default best before days" set') }} {{ $__t('Automatically do the booking using the last price and the amount of the shopping list item, if the product has "Default due days" set') }}
</label> </label>
</div> </div>
</div> </div>

View File

@ -66,7 +66,7 @@
<th>{{ $__t('Product') }}</th> <th>{{ $__t('Product') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<th>{{ $__t('Best before date') }}</th> <th>{{ $__t('Due date') }}</th>
@endif @endif
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@ -83,7 +83,8 @@
<tbody class="d-none"> <tbody class="d-none">
@foreach($stockEntries as $stockEntry) @foreach($stockEntries as $stockEntry)
<tr id="stock-{{ $stockEntry->id }}-row" <tr id="stock-{{ $stockEntry->id }}-row"
class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $stockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days')) data-due-type="{{ FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type }}"
class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $stockEntry->amount > 0) @if(FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->due_type == 1) table-secondary @else table-danger @endif @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $stockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('+' . $nextXDays . ' days'))
&& &&
$stockEntry->amount > 0) table-warning @endif"> $stockEntry->amount > 0) table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
@ -218,8 +219,8 @@
</td> </td>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<td> <td>
<span id="stock-{{ $stockEntry->id }}-best-before-date">{{ $stockEntry->best_before_date }}</span> <span id="stock-{{ $stockEntry->id }}-due-date">{{ $stockEntry->best_before_date }}</span>
<time id="stock-{{ $stockEntry->id }}-best-before-date-timeago" <time id="stock-{{ $stockEntry->id }}-due-date-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
datetime="{{ $stockEntry->best_before_date }} 23:59:59"></time> datetime="{{ $stockEntry->best_before_date }} 23:59:59"></time>
</td> </td>

View File

@ -36,16 +36,16 @@
@include('components.datetimepicker', array( @include('components.datetimepicker', array(
'id' => 'best_before_date', 'id' => 'best_before_date',
'initialValue' => $stockEntry->best_before_date, 'initialValue' => $stockEntry->best_before_date,
'label' => 'Best before', 'label' => 'Due date',
'format' => 'YYYY-MM-DD', 'format' => 'YYYY-MM-DD',
'initWithNow' => false, 'initWithNow' => false,
'limitEndToNow' => false, 'limitEndToNow' => false,
'limitStartToNow' => false, 'limitStartToNow' => false,
'invalidFeedback' => $__t('A best before date is required'), 'invalidFeedback' => $__t('A due date is required'),
'nextInputSelector' => '#best_before_date', 'nextInputSelector' => '#best_before_date',
'additionalGroupCssClasses' => 'date-only-datetimepicker', 'additionalGroupCssClasses' => 'date-only-datetimepicker',
'shortcutValue' => '2999-12-31', 'shortcutValue' => '2999-12-31',
'shortcutLabel' => 'Never expires', 'shortcutLabel' => 'Never overdue',
'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, 'additionalGroupCssClasses' => $additionalGroupCssClasses,

View File

@ -50,10 +50,13 @@
</div> </div>
<div class="border-top border-bottom my-2 py-1"> <div class="border-top border-bottom my-2 py-1">
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<div id="info-expiring-products" <div id="info-duesoon-products"
data-next-x-days="{{ $nextXDays }}" data-next-x-days="{{ $nextXDays }}"
data-status-filter="expiring" data-status-filter="duesoon"
class="warning-message status-filter-message responsive-button mr-2"></div> class="warning-message status-filter-message responsive-button mr-2"></div>
<div id="info-overdue-products"
data-status-filter="overdue"
class="secondary-message status-filter-message responsive-button mr-2"></div>
<div id="info-expired-products" <div id="info-expired-products"
data-status-filter="expired" data-status-filter="expired"
class="error-message status-filter-message responsive-button mr-2"></div> class="error-message status-filter-message responsive-button mr-2"></div>
@ -130,8 +133,9 @@
<option class="bg-white" <option class="bg-white"
value="all">{{ $__t('All') }}</option> value="all">{{ $__t('All') }}</option>
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
<option value="expiring">{{ $__t('Expiring soon') }}</option> <option value="duesoon">{{ $__t('Due soon') }}</option>
<option value="expired">{{ $__t('Already expired') }}</option> <option value="overdue">{{ $__t('Overdue') }}</option>
<option value="expired">{{ $__t('Expired') }}</option>
@endif @endif
<option value="belowminstockamount">{{ $__t('Below min. stock amount') }}</option> <option value="belowminstockamount">{{ $__t('Below min. stock amount') }}</option>
</select> </select>
@ -156,7 +160,7 @@
<th>{{ $__t('Product group') }}</th> <th>{{ $__t('Product group') }}</th>
<th>{{ $__t('Amount') }}</th> <th>{{ $__t('Amount') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Value') }}</th> <th class="@if(!GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) d-none @endif">{{ $__t('Value') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">{{ $__t('Next best before date') }}</th> <th class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">{{ $__t('Next due date') }}</th>
<th class="d-none">Hidden location</th> <th class="d-none">Hidden location</th>
<th class="d-none">Hidden status</th> <th class="d-none">Hidden status</th>
<th class="d-none">Hidden product group</th> <th class="d-none">Hidden product group</th>
@ -172,7 +176,7 @@
<tbody class="d-none"> <tbody class="d-none">
@foreach($currentStock as $currentStockEntry) @foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" <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 ($currentStockEntry->product_missing) table-info @endif"> 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) @if($currentStockEntry->due_type == 1) table-secondary @else table-danger @endif @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 ($currentStockEntry->product_missing) table-info @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" <a class="permission-STOCK_CONSUME btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < $currentStockEntry->quick_consume_amount || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif"
href="#" href="#"
@ -340,8 +344,8 @@
class="locale-number locale-number-currency">{{ $currentStockEntry->value }}</span> class="locale-number locale-number-currency">{{ $currentStockEntry->value }}</span>
</td> </td>
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif"> <td class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">
<span id="product-{{ $currentStockEntry->product_id }}-next-best-before-date">{{ $currentStockEntry->best_before_date }}</span> <span id="product-{{ $currentStockEntry->product_id }}-next-due-date">{{ $currentStockEntry->best_before_date }}</span>
<time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" <time id="product-{{ $currentStockEntry->product_id }}-next-due-date-timeago"
class="timeago timeago-contextual" class="timeago timeago-contextual"
datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time> datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time>
</td> </td>
@ -358,7 +362,7 @@
. ' days' . ' days'
)) ))
&& &&
$currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d $currentStockEntry->amount > 0) @if($currentStockEntry->due_type == 1) overdue @else expired @endif @elseif($currentStockEntry->best_before_date < date('Y-m-d
23:59:59', 23:59:59',
strtotime('+' strtotime('+'
. .
@ -366,7 +370,7 @@
. ' days' . ' days'
)) ))
&& &&
$currentStockEntry->amount > 0) expiring @elseif ($currentStockEntry->product_missing) belowminstockamount @endif" $currentStockEntry->amount > 0) duesoon @elseif ($currentStockEntry->product_missing) belowminstockamount @endif"
</td> </td>
<td class="d-none"> <td class="d-none">
xx{{ $currentStockEntry->product_group_name }}xx xx{{ $currentStockEntry->product_group_name }}xx

View File

@ -59,9 +59,9 @@
<h4 class="mt-2">{{ $__t('Stock overview') }}</h4> <h4 class="mt-2">{{ $__t('Stock overview') }}</h4>
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'stock_expiring_soon_days', 'id' => 'stock_due_soon_days',
'additionalAttributes' => 'data-setting-key="stock_expiring_soon_days"', 'additionalAttributes' => 'data-setting-key="stock_due_soon_days"',
'label' => 'Expiring soon days', 'label' => 'Due soon days',
'min' => 1, 'min' => 1,
'invalidFeedback' => $__t('This cannot be lower than %s', '1'), 'invalidFeedback' => $__t('This cannot be lower than %s', '1'),
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control'
@ -108,11 +108,11 @@
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" <input type="checkbox"
class="form-check-input custom-control-input user-setting-control" class="form-check-input custom-control-input user-setting-control"
id="show_warning_on_purchase_when_best_before_date_is_earlier_than_next" id="show_warning_on_purchase_when_due_date_is_earlier_than_next"
data-setting-key="show_warning_on_purchase_when_best_before_date_is_earlier_than_next"> data-setting-key="show_warning_on_purchase_when_due_date_is_earlier_than_next">
<label class="form-check-label custom-control-label" <label class="form-check-label custom-control-label"
for="show_warning_on_purchase_when_best_before_date_is_earlier_than_next"> for="show_warning_on_purchase_when_due_date_is_earlier_than_next">
{{ $__t('Show a warning when the best before date of the purchased product is earlier than the next best before date in stock') }} {{ $__t('Show a warning when the due date of the purchased product is earlier than the next due date in stock') }}
</label> </label>
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'tasks_due_soon_days', 'id' => 'tasks_due_soon_days',
'additionalAttributes' => 'data-setting-key="tasks_due_soon_days"', 'additionalAttributes' => 'data-setting-key="tasks_due_soon_days"',
'label' => 'Tasks due soon days', 'label' => 'Due soon days',
'min' => 1, 'min' => 1,
'invalidFeedback' => $__t('This cannot be lower than %s', '1'), 'invalidFeedback' => $__t('This cannot be lower than %s', '1'),
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control'

View File

@ -73,7 +73,7 @@
for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }} for="use_specific_stock_entry">{{ $__t('Use a specific stock item') }}
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"') }}"></i> title="{{ $__t('The first item in this list would be picked by the default rule which is "First due first, then first in first out"') }}"></i>
</label> </label>
</div> </div>
<select disabled <select disabled