diff --git a/changelog/57_UNRELEASED_2020-xx-xx.md b/changelog/57_UNRELEASED_2020-xx-xx.md index b8f0e1ed..60dc9a85 100644 --- a/changelog/57_UNRELEASED_2020-xx-xx.md +++ b/changelog/57_UNRELEASED_2020-xx-xx.md @@ -1,6 +1,25 @@ +### New feature: Price history per store +- Define stores under master data +- New product option to set the default store +- Track on purchase/inventory in which store you bought the product (gets prefilled by the last store you purchased the product, or the default store of the product if you never bought it) +- => The price history chart on the product card shows a line per store +- (Thanks @immae and @kriddles) + ### Recipe fixes - Fixed a PHP notice on the recipes page when there are no recipes (thanks @mrunkel) +### Calendar fixes +- Fixed that the "Share/Integrate calendar (iCal)" button did not work (thanks @tsia) + +### API improvements +- The endpoint `/stock/products/{productId}/locations` now also returns the current stock amount of the product in that loctation (new field/property `amount`) (thanks @Forceu) + ### General & other improvements +- New `config.php` setting `FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD` which activates the number pad for best-before-date fields on (supported) mobile browsers (useful because of [shorthands](https://github.com/grocy/grocy#input-shorthands-for-date-fields)) (defaults to `true`) (thanks @Mik-) +- Enhancements for the camera barcode scanner (thanks @Mik-) + - The light button only displayed when the device has a flash light + - New `config.php` setting `FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA` to always enable the flash light automatically + - Various display/CSS improvements - Prerequisites (PHP extensions, critical files/folders) will now be checked and properly reported if there are problems (thanks @Forceu) - Improved the the overview pages on mobile devices (main column was hidden) (thanks @Mik-) +- Optimized the handling of settings provided by `data/settingoverrides` files (thanks @dacto) diff --git a/config-dist.php b/config-dist.php index df69c012..82ae183e 100644 --- a/config-dist.php +++ b/config-dist.php @@ -147,9 +147,11 @@ Setting('FEATURE_FLAG_STOCK_LOCATION_TRACKING', true); Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRODUCT_FREEZING', true); +Setting('FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD', true); // Activate the number pad in best-before-date fields on (supported) mobile browsers Setting('FEATURE_FLAG_SHOPPINGLIST_MULTIPLE_LISTS', true); Setting('FEATURE_FLAG_CHORES_ASSIGNMENTS', true); # Feature settings Setting('FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT', true); // When set to false, opened products will not be considered for minimum stock amounts +Setting('FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA', true); // Enables the torch automaticaly in every camera barcode scanner. diff --git a/controllers/CalendarApiController.php b/controllers/CalendarApiController.php index 183d6245..e20c3cb3 100644 --- a/controllers/CalendarApiController.php +++ b/controllers/CalendarApiController.php @@ -52,7 +52,7 @@ class CalendarApiController extends BaseApiController try { return $this->ApiResponse($response, array( - 'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL)) + 'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(\Grocy\Services\ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL)) )); } catch (\Exception $ex) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 0a652329..5db2038b 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -82,13 +82,19 @@ class StockApiController extends BaseApiController $locationId = $requestBody['location_id']; } + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + $transactionType = StockService::TRANSACTION_TYPE_PURCHASE; if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype'])) { $transactionType = $requestBody['transactiontype']; } - $bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId); + $bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId, $shoppingLocationId); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -144,7 +150,13 @@ class StockApiController extends BaseApiController $locationId = $requestBody['location_id']; } - $bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $price, $requestBody['open'], $requestBody['purchased_date']); + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + + $bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date']); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -312,7 +324,13 @@ class StockApiController extends BaseApiController $price = $requestBody['price']; } - $bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price); + $shoppingLocationId = null; + if (array_key_exists('shopping_location_id', $requestBody) && is_numeric($requestBody['shopping_location_id'])) + { + $shoppingLocationId = $requestBody['shopping_location_id']; + } + + $bookingId = $this->getStockService()->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate, $locationId, $price, $shoppingLocationId); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) diff --git a/controllers/StockController.php b/controllers/StockController.php index 8f3698a2..f42546e6 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -38,6 +38,7 @@ class StockController extends BaseController 'products' => $this->getDatabase()->products()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'), 'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(), 'nextXDays' => $nextXDays, @@ -50,6 +51,7 @@ class StockController extends BaseController { return $this->renderPage($response, 'purchase', [ 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -76,6 +78,7 @@ class StockController extends BaseController { return $this->renderPage($response, 'inventory', [ 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -85,6 +88,7 @@ class StockController extends BaseController return $this->renderPage($response, 'stockentryform', [ 'stockEntry' => $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(), 'products' => $this->getDatabase()->products()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } @@ -140,6 +144,15 @@ class StockController extends BaseController ]); } + public function ShoppingLocationsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + return $this->renderPage($response, 'shoppinglocations', [ + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations'), + 'userfieldValues' => $this->getUserfieldsService()->GetAllValues('shopping_locations') + ]); + } + public function ProductGroupsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { return $this->renderPage($response, 'productgroups', [ @@ -166,6 +179,7 @@ class StockController extends BaseController return $this->renderPage($response, 'productform', [ 'locations' => $this->getDatabase()->locations()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), 'products' => $this->getDatabase()->products()->where('parent_product_id IS NULL')->orderBy('name'), @@ -181,6 +195,7 @@ class StockController extends BaseController 'product' => $product, 'locations' => $this->getDatabase()->locations()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'), + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'), 'userfields' => $this->getUserfieldsService()->GetFields('products'), 'products' => $this->getDatabase()->products()->where('id != :1 AND parent_product_id IS NULL', $product->id)->orderBy('name'), @@ -210,6 +225,25 @@ class StockController extends BaseController } } + public function ShoppingLocationEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + if ($args['shoppingLocationId'] == 'new') + { + return $this->renderPage($response, 'shoppinglocationform', [ + 'mode' => 'create', + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations') + ]); + } + else + { + return $this->renderPage($response, 'shoppinglocationform', [ + 'shoppinglocation' => $this->getDatabase()->shopping_locations($args['shoppingLocationId']), + 'mode' => 'edit', + 'userfields' => $this->getUserfieldsService()->GetFields('shopping_locations') + ]); + } + } + public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { if ($args['productGroupId'] == 'new') diff --git a/data/plugins/DemoBarcodeLookupPlugin.php b/data/plugins/DemoBarcodeLookupPlugin.php index ae2095f0..3d94c832 100644 --- a/data/plugins/DemoBarcodeLookupPlugin.php +++ b/data/plugins/DemoBarcodeLookupPlugin.php @@ -14,7 +14,7 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin /* To try it: - Call the API function at /api/stock/external-barcode-lookup/{barcode} + Call the API function at /api/stock/barcodes/external-lookup/{barcode} When you also add ?add=true as a query parameter to the API call, on a successful lookup the product is added to the database and in the output diff --git a/grocy.openapi.json b/grocy.openapi.json index 194b4765..3797efd3 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1172,6 +1172,11 @@ "format": "integer", "description": "If omitted, the default location of the product is used" }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no store will be affected" + }, "purchased_date": { "type": "string", "format": "date", @@ -1478,6 +1483,11 @@ "type": "number", "format": "integer", "description": "If omitted, the default location of the product is used" + }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no store will be affected" } }, "example": { @@ -1706,6 +1716,11 @@ "format": "date", "description": "The best before date which applies to added products" }, + "shopping_location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, no store will be affected" + }, "location_id": { "type": "number", "format": "integer", @@ -3303,6 +3318,7 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", + "shopping_locations", "recipes", "recipes_pos", "recipes_nestings", @@ -3328,6 +3344,7 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", + "shopping_locations", "recipes", "recipes_pos", "recipes_nestings", @@ -3418,6 +3435,9 @@ "row_created_timestamp": { "type": "string", "format": "date-time" + }, + "shopping_location_id": { + "type": "integer" } }, "example": { @@ -3438,7 +3458,8 @@ "allow_partial_units_in_stock": "0", "enable_tare_weight_handling": "0", "tare_weight": "0.0", - "not_check_stock_fulfillment_for_recipes": "0" + "not_check_stock_fulfillment_for_recipes": "0", + "shopping_location_id": null } }, "QuantityUnit": { @@ -3497,6 +3518,30 @@ "row_created_timestamp": "2019-05-02 20:12:25" } }, + "ShoppingLocation": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "id": "2", + "name": "0", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25" + } + }, "StockLocation": { "type": "object", "properties": { @@ -3506,6 +3551,9 @@ "product_id": { "type": "integer" }, + "amount": { + "type": "integer" + }, "location_id": { "type": "integer" }, @@ -3519,6 +3567,7 @@ "example": { "id": "1", "product_id": "3", + "amount": "2", "location_id": "1", "name": "Fridge" } @@ -3535,6 +3584,9 @@ "location_id": { "type": "integer" }, + "shopping_location_id": { + "type": "integer" + }, "amount": { "type": "number" }, @@ -3576,7 +3628,8 @@ "open": "0", "opened_date": null, "row_created_timestamp": "2019-05-03 18:24:04", - "location_id": "4" + "location_id": "4", + "shopping_location_id": null } }, "RecipeFulfillmentResponse": { @@ -3641,6 +3694,9 @@ "type": "number", "format": "number" }, + "last_shopping_location_id": { + "type": "integer" + }, "location": { "$ref": "#/components/schemas/Location" }, @@ -3672,7 +3728,8 @@ "allow_partial_units_in_stock": "0", "enable_tare_weight_handling": "0", "tare_weight": "0.0", - "not_check_stock_fulfillment_for_recipes": "0" + "not_check_stock_fulfillment_for_recipes": "0", + "last_shopping_location_id": null }, "last_purchased": null, "last_used": null, @@ -3695,6 +3752,7 @@ "plural_forms": null }, "last_price": null, + "last_shopping_location_id": null, "next_best_before_date": "2019-07-07", "location": { "id": "4", @@ -3716,6 +3774,9 @@ "price": { "type": "number", "format": "number" + }, + "shopping_location": { + "$ref": "#/components/schemas/ShoppingLocation" } } }, diff --git a/helpers/extensions.php b/helpers/extensions.php index 5a69eef7..6cdcb203 100644 --- a/helpers/extensions.php +++ b/helpers/extensions.php @@ -138,6 +138,20 @@ function BoolToString(bool $bool) return $bool ? 'true' : 'false'; } +function ExternalSettingValue(string $value) +{ + $tvalue = rtrim($value, "\r\n"); + $lvalue = strtolower($tvalue); + if ($lvalue === "true"){ + return true; + } + elseif ($lvalue === "false") + { + return false; + } + return $tvalue; +} + function Setting(string $name, $value) { if (!defined('GROCY_' . $name)) @@ -146,22 +160,11 @@ function Setting(string $name, $value) $settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt'; if (file_exists($settingOverrideFile)) { - define('GROCY_' . $name, file_get_contents($settingOverrideFile)); + define('GROCY_' . $name, ExternalSettingValue(file_get_contents($settingOverrideFile))); } elseif (getenv('GROCY_' . $name) !== false) // An environment variable with the same name and prefix GROCY_ overwrites the given setting { - if (strtolower(getenv('GROCY_' . $name)) === "true") - { - define('GROCY_' . $name, true); - } - elseif (strtolower(getenv('GROCY_' . $name)) === "false") - { - define('GROCY_' . $name, false); - } - else - { - define('GROCY_' . $name, getenv('GROCY_' . $name)); - } + define('GROCY_' . $name, ExternalSettingValue(getenv('GROCY_'. $name))); } else { diff --git a/localization/demo_data.pot b/localization/demo_data.pot index 282e5160..bd7e4d02 100644 --- a/localization/demo_data.pot +++ b/localization/demo_data.pot @@ -345,3 +345,11 @@ msgstr "" msgid "Portuguese (Portugal)" msgstr "" + +# Use a in your country well known supermarket name +msgid "DemoSupermarket1" +msgstr "" + +# Use a in your country well known supermarket name +msgid "DemoSupermarket2" +msgstr "" diff --git a/localization/en/demo_data.po b/localization/en/demo_data.po index 0d629661..ea4c0cc0 100644 --- a/localization/en/demo_data.po +++ b/localization/en/demo_data.po @@ -284,3 +284,9 @@ msgstr "Swedish" msgid "Polish" msgstr "Polish" + +msgid "DemoSupermarket1" +msgstr "Walmart" + +msgid "DemoSupermarket2" +msgstr "Kroger" diff --git a/localization/en/strings.po b/localization/en/strings.po index 5b5d522a..7f4a8cdf 100644 --- a/localization/en/strings.po +++ b/localization/en/strings.po @@ -66,6 +66,9 @@ msgstr "Products" msgid "Locations" msgstr "Locations" +msgid "Shopping locations" +msgstr "Shopping locations" + msgid "Quantity units" msgstr "Quantity units" @@ -162,6 +165,9 @@ msgstr "Name" msgid "Location" msgstr "Location" +msgid "Shopping location" +msgstr "Shopping location" + msgid "Min. stock amount" msgstr "Min. stock amount" @@ -201,6 +207,9 @@ msgstr "Factor purchase to stock quantity unit" msgid "Create location" msgstr "Create location" +msgid "Create shopping location" +msgstr "Create shopping location" + msgid "Create quantity unit" msgstr "Create quantity unit" @@ -234,6 +243,9 @@ msgstr "Edit product" msgid "Edit location" msgstr "Edit location" +msgid "Edit shopping location" +msgstr "Edit shopping location" + msgid "Record data" msgstr "Record data" @@ -306,6 +318,9 @@ msgstr "Are you sure to delete product \"%s\"?" msgid "Are you sure to delete location \"%s\"?" msgstr "Are you sure to delete location \"%s\"?" +msgid "Are you sure to delete shopping location \"%s\"?" +msgstr "Are you sure to delete shopping location \"%s\"?" + msgid "Manage API keys" msgstr "Manage API keys" @@ -1035,6 +1050,9 @@ msgstr "Tare weight handling enabled - please weigh the whole container, the amo msgid "You have to select a location" msgstr "You have to select a location" +msgid "You have to select a shopping location" +msgstr "You have to select a shopping location" + msgid "List" msgstr "List" diff --git a/localization/fr/strings.po b/localization/fr/strings.po index 4d0839af..7008656a 100644 --- a/localization/fr/strings.po +++ b/localization/fr/strings.po @@ -99,6 +99,9 @@ msgstr "Suivi des piles" msgid "Locations" msgstr "Emplacements" +msgid "Shopping locations" +msgstr "Commerces" + msgid "Quantity units" msgstr "Formats" @@ -198,6 +201,9 @@ msgstr "Nom" msgid "Location" msgstr "Emplacement" +msgid "Shopping location" +msgstr "Commerce" + msgid "Min. stock amount" msgstr "Quantité minimum en stock" @@ -237,6 +243,9 @@ msgstr "Facteur entre la quantité à l'achat et la quantité en stock" msgid "Create location" msgstr "Créer un emplacement" +msgid "Create shopping location" +msgstr "Créer un commerce" + msgid "Create quantity unit" msgstr "Créer un format" @@ -270,6 +279,9 @@ msgstr "Modifier le produit" msgid "Edit location" msgstr "Modifier l'emplacement" +msgid "Edit shopping location" +msgstr "Modifier le commerce" + msgid "Record data" msgstr "Enregistrer les données" @@ -347,6 +359,9 @@ msgstr "Voulez-vous vraiment supprimer le produit \"%s\" ?" msgid "Are you sure to delete location \"%s\"?" msgstr "Voulez-vous vraiment supprimer l'emplacement \"%s\" ?" +msgid "Are you sure to delete shopping location \"%s\"?" +msgstr "Voulez-vous vraiment supprimer le commerce \"%s\" ?" + msgid "Manage API keys" msgstr "Gérer les clefs API" @@ -1124,6 +1139,9 @@ msgstr "" msgid "You have to select a location" msgstr "Vous devez sélectionner un endroit" +msgid "You have to select a shopping location" +msgstr "Vous devez sélectionner un commerce" + msgid "List" msgstr "Liste" diff --git a/localization/strings.pot b/localization/strings.pot index 0cbcdba2..bcb38911 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -79,6 +79,9 @@ msgstr "" msgid "Locations" msgstr "" +msgid "Stores" +msgstr "" + msgid "Quantity units" msgstr "" @@ -214,6 +217,9 @@ msgstr "" msgid "Create location" msgstr "" +msgid "Create store" +msgstr "" + msgid "Create quantity unit" msgstr "" @@ -247,6 +253,9 @@ msgstr "" msgid "Edit location" msgstr "" +msgid "Edit store" +msgstr "" + msgid "Record data" msgstr "" @@ -319,6 +328,9 @@ msgstr "" msgid "Are you sure to delete location \"%s\"?" msgstr "" +msgid "Are you sure to delete store \"%s\"?" +msgstr "" + msgid "Manage API keys" msgstr "" @@ -1022,6 +1034,9 @@ msgstr "" msgid "You have to select a location" msgstr "" +msgid "You have to select a store" +msgstr "" + msgid "List" msgstr "" @@ -1744,3 +1759,15 @@ msgstr "" msgid "Group ingredients by their product group" msgstr "" + +msgid "Unknown store" +msgstr "" + +msgid "Store" +msgstr "" + +msgid "Transaction successfully undone" +msgstr "" + +msgid "Default store" +msgstr "" diff --git a/migrations/0099.sql b/migrations/0099.sql index d9a923c2..b7d90f92 100644 --- a/migrations/0099.sql +++ b/migrations/0099.sql @@ -1,2 +1,30 @@ +CREATE TABLE shopping_locations ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + description TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +ALTER TABLE stock_log +ADD shopping_location_id INTEGER; + +ALTER TABLE stock +ADD shopping_location_id INTEGER; + ALTER TABLE products -ADD picture_url TEXT; \ No newline at end of file +ADD shopping_location_id INTEGER; + +DROP VIEW stock_current_locations; +CREATE VIEW stock_current_locations +AS +SELECT + 1 AS id, -- Dummy, LessQL needs an id column + s.product_id, + SUM(s.amount) as amount, + s.location_id AS location_id, + l.name AS location_name, + l.is_freezer AS location_is_freezer +FROM stock s +JOIN locations l + ON s.location_id = l.id +GROUP BY s.product_id, s.location_id, l.name; diff --git a/migrations/0100.sql b/migrations/0100.sql new file mode 100644 index 00000000..d9a923c2 --- /dev/null +++ b/migrations/0100.sql @@ -0,0 +1,2 @@ +ALTER TABLE products +ADD picture_url TEXT; \ No newline at end of file diff --git a/public/css/grocy.css b/public/css/grocy.css index 68bc85ae..957bdf13 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -259,6 +259,21 @@ input::-webkit-inner-spin-button { color: inherit; } +/* Barcodescanner Quagga */ +#barcodescanner-container { + max-height: 90vw; +} +#livestream-container { + max-height: 100%; +} +#barcodescanner-livestream video { + width: 100%; + +} +#barcodescanner-livestream canvas { + width: 100%; +} + /* Third party component customizations - Bootstrap */ /* Hide the form validation feedback icons introduced in Bootstrap 4.2.0 - a colored border is enough */ diff --git a/public/viewjs/components/barcodescanner.js b/public/viewjs/components/barcodescanner.js index 5db834fa..dc21c717 100644 --- a/public/viewjs/components/barcodescanner.js +++ b/public/viewjs/components/barcodescanner.js @@ -1,5 +1,42 @@ Grocy.Components.BarcodeScanner = { }; +Grocy.Components.BarcodeScanner.CheckCapabilities = function() +{ + var track = Quagga.CameraAccess.getActiveTrack(); + var capabilities = {}; + if (typeof track.getCapabilities === 'function') { + capabilities = track.getCapabilities(); + } + + // Check if the camera is capable to turn on a torch. + var canTorch = typeof capabilities.torch === 'boolean' && capabilities.torch + // Remove the torch button, if either the device can not torch or AutoTorchOn is set. + var node = document.querySelector('.torch'); + if (node) { + node.style.display = canTorch && !Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none'; + } + // If AutoTorchOn is set, turn on the torch. + if (canTorch && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) { + Grocy.Components.BarcodeScanner.TorchOn(track); + } + + // Reduce the height of the video, if it's heigher than then the viewport + var bc = document.getElementById('barcodescanner-container'); + if (bc) { + var bcAspectRatio = bc.offsetWidth / bc.offsetHeight; + var settings = track.getSettings(); + if (bcAspectRatio > settings.aspectRatio) { + var v = document.querySelector('#barcodescanner-livestream video') + if (v) { + var c = document.querySelector('#barcodescanner-livestream canvas') + var newWidth = v.clientWidth / bcAspectRatio * settings.aspectRatio + 'px'; + v.style.width = newWidth; + c.style.width = newWidth; + } + } + } +} + Grocy.Components.BarcodeScanner.StartScanning = function() { Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; @@ -11,8 +48,6 @@ Grocy.Components.BarcodeScanner.StartScanning = function() type: "LiveStream", target: document.querySelector("#barcodescanner-livestream"), constraints: { - width: 436, - height: 327, facingMode: "environment" } }, @@ -70,6 +105,9 @@ Grocy.Components.BarcodeScanner.StartScanning = function() }, 500); return; } + + Grocy.Components.BarcodeScanner.CheckCapabilities(); + Quagga.start(); }); } @@ -84,6 +122,19 @@ Grocy.Components.BarcodeScanner.StopScanning = function() bootbox.hideAll(); } +Grocy.Components.BarcodeScanner.TorchOn = function(track) +{ + if (track) { + track.applyConstraints({ + advanced: [ + { + torch: true + } + ] + }); + } +} + Quagga.onDetected(function(result) { $.each(result.codeResult.decodedCodes, function(id, error) @@ -157,10 +208,10 @@ $(document).on("click", "#barcodescanner-start-button", function(e) buttons: { torch: { label: '', - className: 'btn-warning responsive-button', + className: 'btn-warning responsive-button torch', callback: function() { - Quagga.CameraAccess.getActiveTrack().applyConstraints({ advanced: [{ torch: true }] }); + Grocy.Components.BarcodeScanner.TorchOn(Quagga.CameraAccess.getActiveTrack()); return false; } }, diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 3fd4ff44..d588e127 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -123,12 +123,30 @@ Grocy.Components.ProductCard.Refresh = function(productId) $("#productcard-no-price-data-hint").addClass("d-none"); Grocy.Components.ProductCard.ReInitPriceHistoryChart(); + var datasets = {}; + var chart = Grocy.Components.ProductCard.PriceHistoryChart.data; priceHistoryDataPoints.forEach((dataPoint) => { - Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate()); + var key = __t("Unknown store"); + if (dataPoint.shopping_location) + { + key = dataPoint.shopping_location.name + } + + if (!datasets[key]) { + datasets[key] = [] + } + chart.labels.push(moment(dataPoint.date).toDate()); + datasets[key].push(dataPoint.price); - var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0]; - dataset.data.push(dataPoint.price); + }); + Object.keys(datasets).forEach((key) => { + chart.datasets.push({ + data: datasets[key], + fill: false, + borderColor: "HSL(" + (129 * chart.datasets.length) + ",100%,50%)", + label: key, + }); }); Grocy.Components.ProductCard.PriceHistoryChart.update(); } @@ -160,13 +178,9 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() labels: [ //Date objects // Will be populated in Grocy.Components.ProductCard.Refresh ], - datasets: [{ - data: [ - // Will be populated in Grocy.Components.ProductCard.Refresh - ], - fill: false, - borderColor: '%s7a2b8' - }] + datasets: [ //Datasets + // Will be populated in Grocy.Components.ProductCard.Refresh + ] }, options: { scales: { @@ -194,7 +208,7 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() }] }, legend: { - display: false + display: true } } }); diff --git a/public/viewjs/components/shoppinglocationpicker.js b/public/viewjs/components/shoppinglocationpicker.js new file mode 100644 index 00000000..cc2ae73e --- /dev/null +++ b/public/viewjs/components/shoppinglocationpicker.js @@ -0,0 +1,68 @@ +Grocy.Components.ShoppingLocationPicker = { }; + +Grocy.Components.ShoppingLocationPicker.GetPicker = function() +{ + return $('#shopping_location_id'); +} + +Grocy.Components.ShoppingLocationPicker.GetInputElement = function() +{ + return $('#shopping_location_id_text_input'); +} + +Grocy.Components.ShoppingLocationPicker.GetValue = function() +{ + return $('#shopping_location_id').val(); +} + +Grocy.Components.ShoppingLocationPicker.SetValue = function(value) +{ + Grocy.Components.ShoppingLocationPicker.GetInputElement().val(value); + Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); +} + +Grocy.Components.ShoppingLocationPicker.SetId = function(value) +{ + Grocy.Components.ShoppingLocationPicker.GetPicker().val(value); + Grocy.Components.ShoppingLocationPicker.GetPicker().data('combobox').refresh(); + Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); +} + +Grocy.Components.ShoppingLocationPicker.Clear = function() +{ + Grocy.Components.ShoppingLocationPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetId(null); +} + +$('.shopping-location-combobox').combobox({ + appendId: '_text_input', + bsVersion: '4', + clearIfNoMatch: false +}); + +var prefillByName = Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('prefill-by-name').toString(); +if (typeof prefillByName !== "undefined") +{ + possibleOptionElement = $("#shopping_location_id option:contains(\"" + prefillByName + "\")").first(); + + if (possibleOptionElement.length > 0) + { + $('#shopping_location_id').val(possibleOptionElement.val()); + $('#shopping_location_id').data('combobox').refresh(); + $('#shopping_location_id').trigger('change'); + + var nextInputElement = $(Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('next-input-selector').toString()); + nextInputElement.focus(); + } +} + +var prefillById = Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('prefill-by-id').toString(); +if (typeof prefillById !== "undefined") +{ + $('#shopping_location_id').val(prefillById); + $('#shopping_location_id').data('combobox').refresh(); + $('#shopping_location_id').trigger('change'); + + var nextInputElement = $(Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('next-input-selector').toString()); + nextInputElement.focus(); +} diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 5bed121c..8286b308 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -17,6 +17,7 @@ var jsonData = { }; jsonData.new_amount = jsonForm.new_amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); @@ -84,6 +85,7 @@ $('#price').val(''); Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('inventory-form'); @@ -150,6 +152,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) } $('#price').val(productDetails.last_price); + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { Grocy.Components.LocationPicker.SetId(productDetails.location.id); diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 91dcc3db..e4e8e3b3 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -29,6 +29,7 @@ var jsonData = {}; jsonData.amount = amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); jsonData.price = price; if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { @@ -99,6 +100,7 @@ } Grocy.Components.DateTimePicker.Clear(); Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ShoppingLocationPicker.SetValue(''); Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductCard.Refresh(jsonForm.product_id); Grocy.FrontendHelpers.ValidateForm('purchase-form'); @@ -138,6 +140,16 @@ if (Grocy.Components.ProductPicker !== undefined) function(productDetails) { $('#price').val(productDetails.last_price); + + if (productDetails.last_shopping_location_id != null) + { + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id); + } + else + { + Grocy.Components.ShoppingLocationPicker.SetId(productDetails.default_shopping_location_id); + } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { Grocy.Components.LocationPicker.SetId(productDetails.location.id); diff --git a/public/viewjs/shoppinglocationform.js b/public/viewjs/shoppinglocationform.js new file mode 100644 index 00000000..587981c1 --- /dev/null +++ b/public/viewjs/shoppinglocationform.js @@ -0,0 +1,69 @@ +$('#save-shopping-location-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#shoppinglocation-form').serializeJSON(); + Grocy.FrontendHelpers.BeginUiBusy("shoppinglocation-form"); + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('objects/shopping_locations', jsonData, + function(result) + { + Grocy.EditObjectId = result.created_object_id; + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/shoppinglocations'); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglocation-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + Grocy.Api.Put('objects/shopping_locations/' + Grocy.EditObjectId, jsonData, + function(result) + { + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/shoppinglocations'); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglocation-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } +}); + +$('#shoppinglocation-form input').keyup(function (event) +{ + Grocy.FrontendHelpers.ValidateForm('shoppinglocation-form'); +}); + +$('#shoppinglocation-form input').keydown(function (event) +{ + if (event.keyCode === 13) //Enter + { + event.preventDefault(); + + if (document.getElementById('shoppinglocation-form').checkValidity() === false) //There is at least one validation error + { + return false; + } + else + { + $('#save-shopping-location-button').click(); + } + } +}); + +Grocy.Components.UserfieldsForm.Load(); +$('#name').focus(); +Grocy.FrontendHelpers.ValidateForm('shoppinglocation-form'); diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js new file mode 100644 index 00000000..972bce7d --- /dev/null +++ b/public/viewjs/shoppinglocations.js @@ -0,0 +1,57 @@ +var locationsTable = $('#shoppinglocations-table').DataTable({ + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 }, + { 'searchable': false, "targets": 0 } + ] +}); +$('#shoppinglocations-table tbody').removeClass("d-none"); +locationsTable.columns.adjust().draw(); + +$("#search").on("keyup", Delay(function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + locationsTable.search(value).draw(); +}, 200)); + +$(document).on('click', '.shoppinglocation-delete-button', function (e) +{ + var objectName = $(e.currentTarget).attr('data-shoppinglocation-name'); + var objectId = $(e.currentTarget).attr('data-shoppinglocation-id'); + + bootbox.confirm({ + message: __t('Are you sure to delete store "%s"?', objectName), + closeButton: false, + buttons: { + confirm: { + label: __t('Yes'), + className: 'btn-success' + }, + cancel: { + label: __t('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.Api.Delete('objects/shopping_locations/' + objectId, {}, + function(result) + { + window.location.href = U('/shoppinglocations'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js index 8a1b8579..8f173a05 100644 --- a/public/viewjs/stockentries.js +++ b/public/viewjs/stockentries.js @@ -166,18 +166,35 @@ function RefreshStockEntryRow(stockRowId) function(locationResult) { locationName = locationResult.name; + + $('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id); + $('#stock-' + stockRowId + '-location').text(locationName); }, function(xhr) { console.error(xhr); } ); - $('#stock-' + stockRowId + '-location').attr('data-location-id', result.location_id); - $('#stock-' + stockRowId + '-location').text(locationName); + $('#stock-' + stockRowId + '-price').text(result.price); $('#stock-' + stockRowId + '-purchased-date').text(result.purchased_date); $('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59'); + var shoppingLocationName = ""; + Grocy.Api.Get("objects/shopping_locations/" + result.shopping_location_id, + function(shoppingLocationResult) + { + shoppingLocationName = shoppingLocationResult.name; + + $('#stock-' + stockRowId + '-shopping-location').attr('data-shopping-location-id', result.location_id); + $('#stock-' + stockRowId + '-shopping-location').text(shoppingLocationName); + }, + function (xhr) + { + console.error(xhr); + } + ); + if (result.open == 1) { $('#stock-' + stockRowId + '-opened-amount').text(__t('Opened')); diff --git a/public/viewjs/stockentryform.js b/public/viewjs/stockentryform.js index 0492ee31..4c50e0df 100644 --- a/public/viewjs/stockentryform.js +++ b/public/viewjs/stockentryform.js @@ -14,6 +14,7 @@ jsonData.amount = jsonForm.amount; jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.purchased_date = Grocy.Components.DateTimePicker2.GetValue(); + jsonData.shopping_location_id = Grocy.Components.ShoppingLocationPicker.GetValue(); if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { jsonData.location_id = Grocy.Components.LocationPicker.GetValue(); diff --git a/routes.php b/routes.php index d6a7cddf..8df89867 100644 --- a/routes.php +++ b/routes.php @@ -57,6 +57,13 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting'); } + // Stock price tracking + if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + { + $group->get('/shoppinglocations', '\Grocy\Controllers\StockController:ShoppingLocationsList'); + $group->get('/shoppinglocation/{shoppingLocationId}', '\Grocy\Controllers\StockController:ShoppingLocationEditForm'); + } + // Shopping list routes if (GROCY_FEATURE_FLAG_SHOPPINGLIST) { diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 6a32b9e3..29d426a7 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -41,6 +41,9 @@ class DemoDataGeneratorService extends BaseService INSERT INTO locations (name) VALUES ('{$this->__t_sql('Tinned food cupboard')}'); --5 INSERT INTO locations (name, is_freezer) VALUES ('{$this->__t_sql('Freezer')}', 1); --6 + INSERT INTO shopping_locations (name) VALUES ('{$this->__t_sql('DemoSupermarket1')}'); --1 + INSERT INTO shopping_locations (name) VALUES ('{$this->__t_sql('DemoSupermarket2')}'); --2 + DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Glass')}'; INSERT INTO quantity_units (id, name, name_plural) VALUES (4, '{$this->__n_sql(1, 'Glass', 'Glasses')}', '{$this->__n_sql(2, 'Glass', 'Glasses')}'); --4 DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Tin')}'; @@ -186,80 +189,80 @@ class DemoDataGeneratorService extends BaseService $this->getDatabaseService()->ExecuteDbStatement($sql); $stockService = new StockService(); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); - $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); - $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); - $stockService->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); - $stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(4, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+20 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(6, 1, date('Y-m-d', strtotime('+600 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(7, 1, date('Y-m-d', strtotime('+800 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(8, 1, date('Y-m-d', strtotime('+900 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(9, 1, date('Y-m-d', strtotime('+14 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(10, 1, date('Y-m-d', strtotime('+21 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(11, 1, date('Y-m-d', strtotime('+10 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(12, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(13, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(14, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $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()); + $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()); + $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()); + $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()); + $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()); + $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()); + $stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); + $stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId()); $stockService->AddMissingProductsToShoppingList(); $stockService->OpenProduct(3, 1); $stockService->OpenProduct(6, 1); @@ -317,6 +320,23 @@ class DemoDataGeneratorService extends BaseService return mt_rand(2 * 100, 25 * 100) / 100; } + private $LastSupermarketId = 1; + private function NextSupermarketId() + { + $returnValue = $this->LastSupermarketId; + + if ($this->LastSupermarketId == 1) + { + $this->LastSupermarketId = 2; + } + else + { + $this->LastSupermarketId = 1; + } + + return $returnValue; + } + private function __t_sql(string $text) { $localizedText = $this->getLocalizationService()->__t($text, null); diff --git a/services/StockService.php b/services/StockService.php index bfde3fc2..2b794bf2 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -127,10 +127,13 @@ class StockService extends BaseService $averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = null; + $defaultShoppingLocation = null; + $lastShoppingLocation = null; $lastLogRow = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); if ($lastLogRow !== null && !empty($lastLogRow)) { $lastPrice = $lastLogRow->price; + $lastShoppingLocation = $lastLogRow->shopping_location_id; } $consumeCount = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 0')->sum('amount') * -1; @@ -152,6 +155,8 @@ class StockService extends BaseService 'quantity_unit_purchase' => $quPurchase, 'quantity_unit_stock' => $quStock, 'last_price' => $lastPrice, + 'last_shopping_location_id' => $lastShoppingLocation, + 'default_shopping_location_id' => $product->shopping_location_id, 'next_best_before_date' => $nextBestBeforeDate, 'location' => $location, 'average_shelf_life_days' => $averageShelfLifeDays, @@ -168,12 +173,14 @@ class StockService extends BaseService } $returnData = array(); + $shoppingLocations = $this->getDatabase()->shopping_locations(); $rows = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); foreach ($rows as $row) { $returnData[] = array( 'date' => $row->purchased_date, - 'price' => $row->price + 'price' => $row->price, + 'shopping_location' => FindObjectInArrayByPropertyValue($shoppingLocations, 'id', $row->shopping_location_id), ); } return $returnData; @@ -210,7 +217,7 @@ class StockService extends BaseService return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); } - public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, &$transactionId = null) + public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null) { if (!$this->ProductExists($productId)) { @@ -266,7 +273,8 @@ class StockService extends BaseService 'transaction_type' => $transactionType, 'price' => $price, 'location_id' => $locationId, - 'transaction_id' => $transactionId + 'transaction_id' => $transactionId, + 'shopping_location_id' => $shoppingLocationId, )); $logRow->save(); @@ -279,7 +287,8 @@ class StockService extends BaseService 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, 'price' => $price, - 'location_id' => $locationId + 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, )); $stockRow->save(); @@ -589,7 +598,7 @@ class StockService extends BaseService return $this->getDatabase()->lastInsertId(); } - public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $price, $open, $purchasedDate) + public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate) { $stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch(); @@ -611,6 +620,7 @@ class StockService extends BaseService 'price' => $stockRow->price, 'opened_date' => $stockRow->opened_date, 'location_id' => $stockRow->location_id, + 'shopping_location_id' => $stockRow->shopping_location_id, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id @@ -632,6 +642,7 @@ class StockService extends BaseService 'price' => $price, 'best_before_date' => $bestBeforeDate, 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, 'opened_date' => $openedDate, 'open' => $open, 'purchased_date' => $purchasedDate @@ -647,6 +658,7 @@ class StockService extends BaseService 'price' => $price, 'opened_date' => $stockRow->opened_date, 'location_id' => $locationId, + 'shopping_location_id' => $shoppingLocationId, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id @@ -656,7 +668,7 @@ class StockService extends BaseService return $this->getDatabase()->lastInsertId(); } - public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null) + public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null, $shoppingLocationId = null) { if (!$this->ProductExists($productId)) { @@ -670,6 +682,11 @@ class StockService extends BaseService $price = $productDetails->last_price; } + if ($shoppingLocationId === null) + { + $shoppingLocationId = $productDetails->last_shopping_location_id; + } + // Tare weight handling // The given amount is the new total amount including the container weight (gross) // So assume that the amount in stock is the amount also including the container weight @@ -691,7 +708,7 @@ class StockService extends BaseService $bookingAmount = $newAmount; } - return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId); + return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId, $shoppingLocationId); } else if ($newAmount < $productDetails->stock_amount + $containerWeight) { diff --git a/views/components/datetimepicker.blade.php b/views/components/datetimepicker.blade.php index de2d227e..d3ca83bc 100644 --- a/views/components/datetimepicker.blade.php +++ b/views/components/datetimepicker.blade.php @@ -14,6 +14,7 @@ @php if(!isset($nextInputSelector)) { $nextInputSelector = false; } @endphp @php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp @php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp +@php if(empty($activateNumberPad)) { $activateNumberPad = false; } @endphp
- +@endpush + +@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp +@php if(empty($prefillById)) { $prefillById = ''; } @endphp +@php if(!isset($isRequired)) { $isRequired = false; } @endphp +@php if(empty($hint)) { $hint = ''; } @endphp +@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp + +
+ + +
{{ $__t('You have to select a store') }}
+
diff --git a/views/inventory.blade.php b/views/inventory.blade.php index cef125d7..a7893982 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -50,7 +50,8 @@ 'shortcutLabel' => 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp @@ -66,6 +67,11 @@ 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'isRequired' => false )) + + @include('components.shoppinglocationpicker', array( + 'label' => 'Store', + 'shoppinglocations' => $shoppinglocations + )) @else @endif diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index f236d9e4..0ac57062 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -243,6 +243,14 @@ @endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) +
  • + + + {{ $__t('Stores') }} + +
  • + @endif
  • diff --git a/views/productform.blade.php b/views/productform.blade.php index 9a6276df..3828aad8 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -88,6 +88,15 @@ @endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + @include('components.shoppinglocationpicker', array( + 'label' => 'Default store', + 'shoppinglocations' => $shoppinglocations + )) + @else + + @endif + @php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp @include('components.numberpicker', array( 'id' => 'min_stock_amount', diff --git a/views/purchase.blade.php b/views/purchase.blade.php index 4611e596..aa753772 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -30,6 +30,7 @@ 'nextInputSelector' => '#best_before_date .datetimepicker-input' )) + @php $additionalGroupCssClasses = ''; if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) @@ -51,7 +52,8 @@ 'shortcutLabel' => 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp @@ -84,6 +86,10 @@
  • + @include('components.shoppinglocationpicker', array( + 'label' => 'Store', + 'shoppinglocations' => $shoppinglocations + )) @else @endif diff --git a/views/shoppinglocationform.blade.php b/views/shoppinglocationform.blade.php new file mode 100644 index 00000000..bbe1a35f --- /dev/null +++ b/views/shoppinglocationform.blade.php @@ -0,0 +1,45 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $__t('Edit store')) +@else + @section('title', $__t('Create store')) +@endif + +@section('viewJsName', 'shoppinglocationform') + +@section('content') +
    +
    +

    @yield('title')

    + + + + @if($mode == 'edit') + + @endif + +
    + +
    + + +
    {{ $__t('A name is required') }}
    +
    + +
    + + +
    + + @include('components.userfieldsform', array( + 'userfields' => $userfields, + 'entity' => 'shopping_locations' + )) + + + +
    +
    +
    +@stop diff --git a/views/shoppinglocations.blade.php b/views/shoppinglocations.blade.php new file mode 100644 index 00000000..ed49f777 --- /dev/null +++ b/views/shoppinglocations.blade.php @@ -0,0 +1,73 @@ +@extends('layout.default') + +@section('title', $__t('Stores')) +@section('activeNav', 'shoppinglocations') +@section('viewJsName', 'shoppinglocations') + +@section('content') +
    + +
    +
    + + +
    +
    + +
    +
    + + + + + + + + @include('components.userfields_thead', array( + 'userfields' => $userfields + )) + + + + + @foreach($shoppinglocations as $shoppinglocation) + + + + + + @include('components.userfields_tbody', array( + 'userfields' => $userfields, + 'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id) + )) + + + @endforeach + +
    {{ $__t('Name') }}{{ $__t('Description') }}
    + + + + + + + + {{ $shoppinglocation->name }} + + {{ $shoppinglocation->description }} +
    +
    +
    +@stop diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index 7b6566fb..36fda7bb 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -35,7 +35,10 @@ {{ $__t('Amount') }} {{ $__t('Best before date') }} @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING){{ $__t('Location') }}@endif - @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING){{ $__t('Price') }}@endif + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + {{ $__t('Store') }} + {{ $__t('Price') }} + @endif {{ $__t('Purchased date') }} @include('components.userfields_thead', array( @@ -142,6 +145,11 @@ @endif @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + + @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id) !== null) + {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }} + @endif + {{ $stockEntry->price }} diff --git a/views/stockentryform.blade.php b/views/stockentryform.blade.php index c763337b..9b6035f3 100644 --- a/views/stockentryform.blade.php +++ b/views/stockentryform.blade.php @@ -40,7 +40,8 @@ 'shortcutLabel' => 'Never expires', 'earlierThanInfoLimit' => date('Y-m-d'), 'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'), - 'additionalGroupCssClasses' => $additionalGroupCssClasses + 'additionalGroupCssClasses' => $additionalGroupCssClasses, + 'activateNumberPad' => GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_FIELD_NUMBER_PAD )) @php $additionalGroupCssClasses = ''; @endphp @@ -65,6 +66,11 @@ 'invalidFeedback' => $__t('The price cannot be lower than %s', '0'), 'isRequired' => false )) + @include('components.shoppinglocationpicker', array( + 'label' => 'Store', + 'shoppinglocations' => $shoppinglocations, + 'prefillById' => $stockEntry->shopping_location_id + )) @else @endif