merged master

only conflict was a migration script
This commit is contained in:
Zeb Fross 2020-03-29 22:02:20 -07:00
commit 5d980a9bd7
38 changed files with 902 additions and 125 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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"
}
}
},

View File

@ -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
{

View File

@ -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 ""

View File

@ -284,3 +284,9 @@ msgstr "Swedish"
msgid "Polish"
msgstr "Polish"
msgid "DemoSupermarket1"
msgstr "Walmart"
msgid "DemoSupermarket2"
msgstr "Kroger"

View File

@ -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"

View File

@ -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"

View File

@ -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 ""

View File

@ -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;
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;

2
migrations/0100.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE products
ADD picture_url TEXT;

View File

@ -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 */

View File

@ -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: '<i class="far fa-lightbulb"></i>',
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;
}
},

View File

@ -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
}
}
});

View File

@ -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();
}

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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);
}
);
}
}
});
});

View File

@ -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'));

View File

@ -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();

View File

@ -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)
{

View File

@ -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);

View File

@ -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)
{

View File

@ -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
<div id="datetimepicker-wrapper" class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">{{ $__t($label) }}
@ -24,7 +25,7 @@
</label>
<div class="input-group">
<div class="input-group date datetimepicker @if(!empty($additionalGroupCssClasses)){{ $additionalGroupCssClasses }}@endif" id="{{ $id }}" @if(!$noNameAttribute) name="{{ $id }}" @endif data-target-input="nearest">
<input {!! $additionalAttributes !!} type="text" @if($isRequired) @if($isRequired) required @endif @endif class="form-control datetimepicker-input @if(!empty($additionalCssClasses)){{ $additionalCssClasses }}@endif"
<input {!! $additionalAttributes !!} type="text" @if($activateNumberPad) inputmode="numeric" @endif @if($isRequired) @if($isRequired) required @endif @endif class="form-control datetimepicker-input @if(!empty($additionalCssClasses)){{ $additionalCssClasses }}@endif"
data-target="#{{ $id }}" data-format="{{ $format }}"
data-init-with-now="{{ BoolToString($initWithNow) }}"
data-init-value="{{ $initialValue }}"

View File

@ -0,0 +1,20 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/shoppinglocationpicker.js', true) }}?v={{ $version }}"></script>
@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
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
<label for="shopping_location_id">{{ $__t($label) }}&nbsp;&nbsp;<span id="{{ $hintId }}" class="small text-muted">{{ $hint }}</span></label>
<select class="form-control shopping-location-combobox" id="shopping_location_id" name="shopping_location_id" @if($isRequired) required @endif>
<option value=""></option>
@foreach($shoppinglocations as $shoppinglocation)
<option value="{{ $shoppinglocation->id }}">{{ $shoppinglocation->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('You have to select a store') }}</div>
</div>

View File

@ -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
<input type="hidden" name="price" id="price" value="0">
@endif

View File

@ -243,6 +243,14 @@
</a>
</li>
@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<li data-nav-for-page="shoppinglocations" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/shoppinglocations') }}">
<i class="fas fa-shopping-cart"></i>
<span class="nav-link-text">{{ $__t('Stores') }}</span>
</a>
</li>
@endif
<li data-nav-for-page="quantityunits" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/quantityunits') }}">
<i class="fas fa-balance-scale"></i>

View File

@ -88,6 +88,15 @@
<input type="hidden" name="location_id" id="location_id" value="1">
@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.shoppinglocationpicker', array(
'label' => 'Default store',
'shoppinglocations' => $shoppinglocations
))
@else
<input type="hidden" name="shopping_location_id" id="shopping_location_id" value="1">
@endif
@php if($mode == 'edit') { $value = $product->min_stock_amount; } else { $value = 0; } @endphp
@include('components.numberpicker', array(
'id' => 'min_stock_amount',

View File

@ -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 @@
<input class="form-check-input" type="radio" name="price-type" id="price-type-total-price" value="total-price">
<label class="form-check-label" for="price-type-total-price">{{ $__t('Total price') }}</label>
</div>
@include('components.shoppinglocationpicker', array(
'label' => 'Store',
'shoppinglocations' => $shoppinglocations
))
@else
<input type="hidden" name="price" id="price" value="0">
@endif

View File

@ -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')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $shoppinglocation->id }};</script>
@endif
<form id="shoppinglocation-form" novalidate>
<div class="form-group">
<label for="name">{{ $__t('Name') }}</label>
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $shoppinglocation->name }}@endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $shoppinglocation->description }}@endif</textarea>
</div>
@include('components.userfieldsform', array(
'userfields' => $userfields,
'entity' => 'shopping_locations'
))
<button id="save-shopping-location-button" class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@ -0,0 +1,73 @@
@extends('layout.default')
@section('title', $__t('Stores'))
@section('activeNav', 'shoppinglocations')
@section('viewJsName', 'shoppinglocations')
@section('content')
<div class="row">
<div class="col">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/shoppinglocation/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $__t('Add') }}
</a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=shoppinglocations') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $__t('Configure userfields') }}
</a>
</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $__t('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="shoppinglocations-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr>
</thead>
<tbody class="d-none">
@foreach($shoppinglocations as $shoppinglocation)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/shoppinglocation/') }}{{ $shoppinglocation->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm shoppinglocation-delete-button" href="#" data-shoppinglocation-id="{{ $shoppinglocation->id }}" data-shoppinglocation-name="{{ $shoppinglocation->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $shoppinglocation->name }}
</td>
<td>
{{ $shoppinglocation->description }}
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $shoppinglocation->id)
))
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@ -35,7 +35,10 @@
<th>{{ $__t('Amount') }}</th>
<th>{{ $__t('Best before date') }}</th>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<th>{{ $__t('Price') }}</th>@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<th>{{ $__t('Store') }}</th>
<th>{{ $__t('Price') }}</th>
@endif
<th>{{ $__t('Purchased date') }}</th>
@include('components.userfields_thead', array(
@ -142,6 +145,11 @@
</td>
@endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<td id="stock-{{ $stockEntry->id }}-shopping-location" data-shopping-location-id="{{ $stockEntry->shopping_location_id }}">
@if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id) !== null)
{{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $stockEntry->shopping_location_id)->name }}
@endif
</td>
<td id="stock-{{ $stockEntry->id }}-price" class="locale-number locale-number-currency" data-price-id="{{ $stockEntry->price }}">
{{ $stockEntry->price }}
</td>

View File

@ -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
<input type="hidden" name="price" id="price" value="0">
@endif