Qu factor purchase to stock & Product Barcode Details (#801)

* Puchase add qu_factor_to_stock

* qu_factor_purchase_to_stock for stock edit

* product barcodes with QU and Stores

* remove product barcode tags

* migrations/0103 add value and factor_puchase_amount to stock_current and stock_current_location_content

* Remove unused method

* StockService#GetProductDetails: include stock_value

* productcard: include stock_value

* Add Purchase Factor to Stock Overview

* update demo data with stock qu_factor_purchase_to_stock

* recipes_pos_resolved update

* avg_price and oldest_price in product details

* add average price to product card

* hint for recipe costs not included if not in stock

* Round value and factor_purchas_amount. Include currency for stock value

* Add factor_purchase_amount to product card stock amount

* Allow editing qu_factor_purchase_to_stock for stock entries

* fix update qu_factor_purchase_to_stock for Transfers

* Add barcode to existing product update to add to product_barcodes table

* Add barcode to new product workflow update to add to product_barcodes table

* *** Price now saved as 1 QU to stock in stock tables ***

* remove column product barcode and use product_barcodes

* Allow products to be deactivated instead of deleted

* Embedded barcode and qu-conversion with page reload on change

* Save current product barcode into new product_barcodes table

* Embedded popup for product group add/edit

* barcode scanner added to product barcodes input

* Edit product qu_stock is unavailable after first purchase

* StockOverview: Filters break when columns are reordered so for now just disable colReorder

* view stockoverview.blade: display product_group column

* Review

Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
kriddles 2020-08-17 14:47:33 -05:00 committed by GitHub
parent d1e395b45e
commit e8845fe2e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1216 additions and 266 deletions

View File

@ -23,6 +23,18 @@ class StockApiController extends BaseApiController
}
}
public function ProductBarcodeDetails(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
return $this->ApiResponse($response, $this->getDatabase()->product_barcodes()->where('barcode = :1', $args['barcode'])->fetch());
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function ProductDetailsByBarcode(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
@ -88,13 +100,19 @@ class StockApiController extends BaseApiController
$shoppingLocationId = $requestBody['shopping_location_id'];
}
$quFactorPurchaseToStock = null;
if (array_key_exists('qu_factor_purchase_to_stock', $requestBody) && is_numeric($requestBody['qu_factor_purchase_to_stock']))
{
$quFactorPurchaseToStock = $requestBody['qu_factor_purchase_to_stock'];
}
$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, $shoppingLocationId);
$bookingId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $quFactorPurchaseToStock, $locationId, $shoppingLocationId);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)
@ -156,7 +174,7 @@ class StockApiController extends BaseApiController
$shoppingLocationId = $requestBody['shopping_location_id'];
}
$bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date']);
$bookingId = $this->getStockService()->EditStockEntry($args['entryId'], $requestBody['amount'], $bestBeforeDate, $locationId, $shoppingLocationId, $price, $requestBody['open'], $requestBody['purchased_date'], $requestBody['qu_factor_purchase_to_stock']);
return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId));
}
catch (\Exception $ex)

View File

@ -16,7 +16,7 @@ class StockController extends BaseController
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expring_soon_days'];
return $this->renderPage($response, 'stockoverview', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'currentStock' => $this->getStockService()->GetCurrentStock(true),
@ -36,7 +36,7 @@ class StockController extends BaseController
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expring_soon_days'];
return $this->renderPage($response, 'stockentries', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
@ -50,8 +50,12 @@ class StockController extends BaseController
public function Purchase(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$sql = 'select group_concat(barcode) barcodes, product_id from product_barcodes group by product_id';
$productBarcodes = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->renderPage($response, 'purchase', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'barcodes' => $productBarcodes,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
@ -59,8 +63,12 @@ class StockController extends BaseController
public function Consume(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$sql = 'select group_concat(barcode) barcodes, product_id from product_barcodes group by product_id';
$productBarcodes = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->renderPage($response, 'consume', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'barcodes' => $productBarcodes,
'recipes' => $this->getDatabase()->recipes()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
@ -68,8 +76,12 @@ class StockController extends BaseController
public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$sql = 'select group_concat(barcode) barcodes, product_id from product_barcodes group by product_id';
$productBarcodes = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->renderPage($response, 'transfer', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'barcodes' => $productBarcodes,
'recipes' => $this->getDatabase()->recipes()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
@ -77,8 +89,12 @@ class StockController extends BaseController
public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$sql = 'select group_concat(barcode) barcodes, product_id from product_barcodes group by product_id';
$productBarcodes = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
return $this->renderPage($response, 'inventory', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'barcodes' => $productBarcodes,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
@ -88,7 +104,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'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name')
]);
@ -104,7 +120,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'shoppinglist', [
'listItems' => $this->getDatabase()->shopping_list()->where('shopping_list_id = :1', $listId),
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'missingProducts' => $this->getStockService()->GetMissingProducts(),
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name'),
@ -158,7 +174,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'productgroups', [
'productGroups' => $this->getDatabase()->product_groups()->orderBy('name'),
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'userfields' => $this->getUserfieldsService()->GetFields('product_groups'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('product_groups')
]);
@ -179,11 +195,12 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'productform', [
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'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'),
'products' => $this->getDatabase()->products()->where('parent_product_id IS NULL and active = 1')->orderBy('name'),
'isSubProductOfOthers' => false,
'mode' => 'create'
]);
@ -195,11 +212,12 @@ class StockController extends BaseController
return $this->renderPage($response, 'productform', [
'product' => $product,
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'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'),
'products' => $this->getDatabase()->products()->where('id != :1 AND parent_product_id IS NULL and active = 1', $product->id)->orderBy('name'),
'isSubProductOfOthers' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
'mode' => 'edit',
'quConversions' => $this->getDatabase()->quantity_unit_conversions()
@ -296,7 +314,7 @@ class StockController extends BaseController
if ($args['itemId'] == 'new')
{
return $this->renderPage($response, 'shoppinglistitemform', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name'),
'mode' => 'create'
]);
@ -305,7 +323,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'shoppinglistitemform', [
'listItem' => $this->getDatabase()->shopping_list($args['itemId']),
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name'),
'mode' => 'edit'
]);
@ -339,7 +357,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'stockjournal', [
'stockLog' => $this->getDatabase()->stock_log()->orderBy('row_created_timestamp', 'DESC'),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name')
]);
}
@ -347,13 +365,41 @@ class StockController extends BaseController
public function LocationContentSheet(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'locationcontentsheet', [
'products' => $this->getDatabase()->products()->orderBy('name'),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'currentStockLocationContent' => $this->getStockService()->GetCurrentStockLocationContent()
]);
}
public function ProductBarcodesEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$product = null;
if (isset($request->getQueryParams()['product']))
{
$product = $this->getDatabase()->products($request->getQueryParams()['product']);
}
if ($args['productBarcodeId'] == 'new')
{
return $this->renderPage($response, 'productbarcodesform', [
'mode' => 'create',
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
]);
}
else
{
return $this->renderPage($response, 'productbarcodesform', [
'mode' => 'edit',
'barcode' => $this->getDatabase()->product_barcodes($args['productBarcodeId']),
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
]);
}
}
public function QuantityUnitConversionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$product = null;

View File

@ -2703,6 +2703,57 @@
}
}
},
"/productbarcodedetails/{barcode}": {
"get": {
"summary": "Executes a product barcode details lookoup via the configured plugin with the given barcode",
"tags": [
"Product"
],
"parameters": [
{
"in": "path",
"name": "barcode",
"required": true,
"description": "The barcode to lookup up",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "add",
"required": false,
"description": "When true, the product is added to the database on a successful lookup and the new product id is in included in the response",
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
"200": {
"description": "An ProductBarcodeDetails object or null, when nothing was found for the given barcode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductBarcodeDetailsResponse"
}
}
}
},
"400": {
"description": "The operation was not successful (possible errors are: Plugin error)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericErrorResponse"
}
}
}
}
}
}
},
"/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": {
"post": {
"summary": "Adds all missing products for the given recipe to the shopping list",
@ -3374,6 +3425,7 @@
"enum": [
"products",
"chores",
"product_barcodes",
"batteries",
"locations",
"quantity_units",
@ -3399,6 +3451,7 @@
"type": "string",
"enum": [
"products",
"product_barcodes",
"chores",
"batteries",
"locations",
@ -3748,6 +3801,9 @@
"product": {
"$ref": "#/components/schemas/Product"
},
"product_barcodes": {
"$ref": "#/components/schemas/ProductBarcodeDetailsResponse"
},
"quantity_unit_purchase": {
"$ref": "#/components/schemas/QuantityUnit"
},
@ -3776,6 +3832,14 @@
"type": "number",
"format": "number"
},
"avg_price": {
"type": "number",
"format": "number"
},
"oldest_price": {
"type": "number",
"format": "number"
},
"last_shopping_location_id": {
"type": "integer"
},
@ -3813,6 +3877,15 @@
"not_check_stock_fulfillment_for_recipes": "0",
"last_shopping_location_id": null
},
"product_barcodes": [
{
"id": "1",
"product_id": "13",
"barcode": "01321230213",
"qu_factor_purchase_to_stock": "10.0",
"shopping_location_id": "2"
}
],
"last_purchased": null,
"last_used": null,
"stock_amount": "2",
@ -3834,6 +3907,8 @@
"plural_forms": null
},
"last_price": null,
"avg_price": null,
"oldest_price": null,
"last_shopping_location_id": null,
"next_best_before_date": "2019-07-07",
"location": {
@ -3862,6 +3937,28 @@
}
}
},
"ProductBarcodeDetailsResponse": {
"type": "object",
"properties": {
"product_id": {
"type": "integer"
},
"barcode": {
"type": "string"
},
"qu_factor_purchase_to_stock": {
"type": "number",
"format": "number"
},
"barcode": {
"type": "string",
"description": "Can contain multiple barcodes separated by comma"
},
"shopping_location_id": {
"type": "integer"
}
}
},
"ExternalBarcodeLookupResponse": {
"type": "object",
"properties": {

View File

@ -1810,3 +1810,33 @@ msgstr ""
msgid "Save & return to recipes"
msgstr ""
msgid "Stock value"
msgstr ""
msgid "Average price"
msgstr ""
msgid "Active"
msgstr ""
msgid "Barcodes"
msgstr ""
msgid "Barcode"
msgstr ""
msgid "Create Barcode"
msgstr ""
msgid "Barcode for product"
msgstr ""
msgid "Edit Barcode"
msgstr ""
msgid "Not enough in stock (not included in costs), %s ingredient missing"
msgstr ""
msgid "Based on the prices of the default consume rule which is \"First expiring first, then first in first out\""
msgstr ""

171
migrations/0103.sql Normal file
View File

@ -0,0 +1,171 @@
ALTER TABLE stock_log
ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
ALTER TABLE stock
ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
UPDATE stock
SET qu_factor_purchase_to_stock = (SELECT qu_factor_purchase_to_stock FROM products WHERE product_id = id);
UPDATE stock_log
SET qu_factor_purchase_to_stock = (SELECT qu_factor_purchase_to_stock FROM products WHERE product_id = id);
--Price is now going forward to be saved as 1 QU Stock
UPDATE stock
SET price = ROUND(price / qu_factor_purchase_to_stock, 2);
UPDATE stock_log
SET price = ROUND(price / qu_factor_purchase_to_stock, 2);
CREATE TABLE product_barcodes (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INT NOT NULL,
barcode TEXT NOT NULL UNIQUE,
qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1,
shopping_location_id INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
-- Convert product table to new product_barcodes table
INSERT INTO product_barcodes
(product_id, barcode, qu_factor_purchase_to_stock, shopping_location_id)
WITH barcodes_splitted(id, barcode, str, qu_factor_purchase_to_stock, shopping_location_id) AS (
SELECT id as product_id, '', barcode || ',', qu_factor_purchase_to_stock, shopping_location_id
FROM products
UNION ALL
SELECT
id as product_id,
SUBSTR(str, 0, instr(str, ',')),
SUBSTR(str, instr(str, ',') + 1),
qu_factor_purchase_to_stock,
shopping_location_id
FROM barcodes_splitted
WHERE str != ''
)
SELECT id as product_id, barcode, qu_factor_purchase_to_stock, shopping_location_id
FROM barcodes_splitted
WHERE barcode != '';
PRAGMA legacy_alter_table = ON;
ALTER TABLE products RENAME TO products_old;
-- Remove barcode column
-- Reorder columns
CREATE TABLE products (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
product_group_id INTEGER,
active TINYINT NOT NULL DEFAULT 1,
location_id INTEGER NOT NULL,
shopping_location_id INTEGER,
qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL,
min_stock_amount INTEGER NOT NULL DEFAULT 0,
default_best_before_days INTEGER NOT NULL DEFAULT 0,
default_best_before_days_after_open INTEGER NOT NULL DEFAULT 0,
default_best_before_days_after_freezing INTEGER NOT NULL DEFAULT 0,
default_best_before_days_after_thawing INTEGER NOT NULL DEFAULT 0,
picture_file_name TEXT,
allow_partial_units_in_stock TINYINT NOT NULL DEFAULT 0,
enable_tare_weight_handling TINYINT NOT NULL DEFAULT 0,
tare_weight REAL NOT NULL DEFAULT 0,
not_check_stock_fulfillment_for_recipes TINYINT DEFAULT 0,
parent_product_id INT,
calories INTEGER,
cumulate_min_stock_amount_of_sub_products TINYINT DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO products
(id, name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, default_best_before_days, row_created_timestamp, product_group_id, picture_file_name, default_best_before_days_after_open, allow_partial_units_in_stock, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, default_best_before_days_after_freezing, default_best_before_days_after_thawing, shopping_location_id)
SELECT id, name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount,default_best_before_days, row_created_timestamp, product_group_id, picture_file_name, default_best_before_days_after_open, allow_partial_units_in_stock, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, default_best_before_days_after_freezing, default_best_before_days_after_thawing, shopping_location_id
FROM products_old;
DROP TABLE products_old;
DROP VIEW stock_current_location_content;
CREATE VIEW stock_current_location_content
AS
SELECT
IFNULL(s.location_id, p.location_id) AS location_id,
s.product_id,
SUM(s.amount) AS amount,
ROUND(SUM(s.amount / s.qu_factor_purchase_to_stock),2) as factor_purchase_amount,
ROUND(SUM(IFNULL(s.price, 0) * s.amount), 2) AS value,
MIN(s.best_before_date) AS best_before_date,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND location_id = s.location_id AND open = 1), 0) AS amount_opened
FROM stock s
JOIN products p
ON s.product_id = p.id
AND p.active = 1
GROUP BY IFNULL(s.location_id, p.location_id), s.product_id;
DROP VIEW stock_current;
CREATE VIEW stock_current
AS
SELECT
pr.parent_product_id AS product_id,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id), 0) AS amount,
IFNULL(ROUND((SELECT SUM(amount / qu_factor_purchase_to_stock) FROM stock WHERE product_id = pr.parent_product_id), 2), 0) as factor_purchase_amount,
SUM(s.amount) * IFNULL(qucr.factor, 1) AS amount_aggregated,
IFNULL(ROUND((SELECT SUM(IFNULL(price,0) * amount) FROM stock WHERE product_id = pr.parent_product_id), 2), 0) AS value,
MIN(s.best_before_date) AS best_before_date,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id AND open = 1), 0) AS amount_opened,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = pr.parent_product_id) AND open = 1), 0) * IFNULL(qucr.factor, 1) AS amount_opened_aggregated,
CASE WHEN p_sub.parent_product_id IS NOT NULL THEN 1 ELSE 0 END AS is_aggregated_amount
FROM products_resolved pr
JOIN stock s
ON pr.sub_product_id = s.product_id
JOIN products p_parent
ON pr.parent_product_id = p_parent.id
AND p_parent.active = 1
JOIN products p_sub
ON pr.sub_product_id = p_sub.id
AND p_sub.active = 1
LEFT JOIN quantity_unit_conversions_resolved qucr
ON pr.sub_product_id = qucr.product_id
AND p_sub.qu_id_stock = qucr.from_qu_id
AND p_parent.qu_id_stock = qucr.to_qu_id
GROUP BY pr.parent_product_id
HAVING SUM(s.amount) > 0
UNION
-- This is the same as above but sub products not rolled up (no QU conversion and column is_aggregated_amount = 0 here)
SELECT
pr.sub_product_id AS product_id,
SUM(s.amount) AS amount,
ROUND(SUM(s.amount / s.qu_factor_purchase_to_stock), 2) as factor_purchase_amount,
SUM(s.amount) AS amount_aggregated,
ROUND(SUM(IFNULL(s.price, 0) * s.amount), 2) AS value,
MIN(s.best_before_date) AS best_before_date,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened,
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened_aggregated,
0 AS is_aggregated_amount
FROM products_resolved pr
JOIN stock s
ON pr.sub_product_id = s.product_id
WHERE pr.parent_product_id != pr.sub_product_id
GROUP BY pr.sub_product_id
HAVING SUM(s.amount) > 0;
DROP VIEW products_resolved;
CREATE VIEW products_resolved AS
SELECT
p.parent_product_id parent_product_id,
p.id as sub_product_id
FROM products p
WHERE p.parent_product_id IS NOT NULL
AND p.active = 1
UNION
SELECT
p.id parent_product_id,
p.id as sub_product_id
FROM products p
WHERE p.parent_product_id IS NULL
AND p.active = 1;

185
migrations/0104.sql Normal file
View File

@ -0,0 +1,185 @@
-- Deprecate unused view to instead use products_last_purchased
DROP VIEW products_current_price;
CREATE VIEW products_last_purchased
AS
select
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.amount,
sl.best_before_date,
sl.purchased_date,
sl.price,
sl.qu_factor_purchase_to_stock,
sl.location_id,
sl.shopping_location_id
from stock_log sl
JOIN (
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s1.product_id) sp3
ON sl.product_id = sp3.product_id
AND sl.id = sp3.max_stock_id;
CREATE VIEW products_average_price
AS
SELECT
1 AS id, -- Dummy, LessQL needs an id column
s.product_id,
round(sum(s.amount * s.price) / sum(s.amount), 2) as price
FROM stock s
GROUP BY s.product_id;
CREATE VIEW products_oldest_stock_unit_price
AS
-- Find oldest best_before_date then oldest purchased_date then make sure to return one stock row using max
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sw.product_id,
sw.amount,
sw.best_before_date,
sw.purchased_date,
sw.price, sw.qu_factor_purchase_to_stock,
sw.location_id,
sw.shopping_location_id
FROM stock sw
JOIN (
SELECT
s1.product_id,
MIN(s1.id) min_stock_id
FROM stock s1
JOIN (
SELECT
s.product_id,
sp.oldest_date,
MIN(s.purchased_date) min_purchased_date
FROM stock s
JOIN (
SELECT
product_id,
MIN(best_before_date) as oldest_date
FROM stock
GROUP BY product_id) sp
ON s.product_id = sp.product_id
AND s.best_before_date = sp.oldest_date
GROUP BY s.product_id, sp.oldest_date) sp2
ON s1.product_id = sp2.product_id
AND s1.best_before_date = sp2.oldest_date
AND s1.purchased_date = sp2.min_purchased_date
GROUP BY s1.product_id) sp3
ON sw.product_id = sp3.product_id
AND sw.id = sp3.min_stock_id;
DROP VIEW recipes_pos_resolved;
CREATE VIEW recipes_pos_resolved
AS
-- Multiplication by 1.0 to force conversion to float (REAL)
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled,
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount,
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
CASE WHEN IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * pop.price * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 0
UNION
-- Just add all recipe positions which should not be checked against stock with fulfilled need
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
1 AS need_fulfilled,
0 AS missing_amount,
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
1 AS need_fulfilled_with_shopping_list,
rp.qu_id,
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * IFNULL(pop.price, 0) * rp.price_factor AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
p.active AS product_active
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_oldest_stock_unit_price pop
ON rp.product_id = pop.product_id
WHERE rp.not_check_stock_fulfillment = 1;

View File

@ -6,11 +6,24 @@ Grocy.Components.ProductCard.Refresh = function(productId)
function(productDetails)
{
var stockAmount = productDetails.stock_amount || '0';
var stockFactorPurchaseAmount = productDetails.stock_factor_purchase_amount || '0';
var stockValue = productDetails.stock_value || '0';
var stockAmountOpened = productDetails.stock_amount_opened || '0';
$('#productcard-product-name').text(productDetails.product.name);
$('#productcard-product-description').html(productDetails.product.description);
$('#productcard-product-stock-amount').text(stockAmount);
$('#productcard-product-stock-qu-name').text(__n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural));
if (productDetails.last_qu_factor_purchase_to_stock > 1)
{
$('#productcard-product-stock-factor-purchase-amount').text('(' + stockFactorPurchaseAmount);
$('#productcard-product-stock-factor-purchase-qu-name').text(__n(stockFactorPurchaseAmount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural) + ')');
}
else
{
$('#productcard-product-stock-factor-purchase-amount').text('');
$('#productcard-product-stock-factor-purchase-qu-name').text('');
}
$('#productcard-product-stock-value').text(stockValue + ' ' + Grocy.Currency);
$('#productcard-product-last-purchased').text((productDetails.last_purchased || '2999-12-31').substring(0, 10));
$('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || '2999-12-31');
$('#productcard-product-last-used').text((productDetails.last_used || '2999-12-31').substring(0, 10));
@ -80,13 +93,29 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null)
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
if (productDetails.last_qu_factor_purchase_to_stock > 1)
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per 1 ' + productDetails.quantity_unit_purchase.name + ' of ' + productDetails.last_qu_factor_purchase_to_stock + ' ' + productDetails.quantity_unit_stock.name_plural);
}
else
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
}
}
else
{
$('#productcard-product-last-price').text(__t('Unknown'));
}
if (productDetails.avg_price !== null)
{
$('#productcard-product-average-price').text(Number.parseFloat(productDetails.avg_price).toLocaleString() + ' ' + Grocy.Currency);
}
else
{
$('#productcard-product-average-price').text(__t('Unknown'));
}
if (productDetails.product.picture_file_name !== null && !productDetails.product.picture_file_name.isEmpty())
{
$("#productcard-product-picture").removeClass("d-none");

View File

@ -132,6 +132,7 @@ $('#product_id_text_input').on('blur', function(e)
{
return;
}
$('#product_id').attr("barcode", "null");
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
@ -139,6 +140,7 @@ $('#product_id_text_input').on('blur', function(e)
if (GetUriParam('addbarcodetoselection') === undefined && input.length > 0 && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').attr("barcode", input);
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}

View File

@ -44,17 +44,12 @@
var addBarcode = GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
var jsonDataBarcode = {};
jsonDataBarcode.barcode = addBarcode;
jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
Grocy.Api.Put('objects/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result)
{
$("#flow-info-addbarcodetoselection").addClass("d-none");
@ -63,7 +58,8 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.EndUiBusy("consume-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}

View File

@ -37,17 +37,13 @@
var addBarcode = GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
var jsonDataBarcode = {};
jsonDataBarcode.barcode = addBarcode;
jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id;
Grocy.Api.Put('objects/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result)
{
$("#flow-info-addbarcodetoselection").addClass("d-none");
@ -56,7 +52,8 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.EndUiBusy("inventory-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}

View File

@ -180,6 +180,11 @@ var calendar = $("#calendar").fullCalendar({
productDetails.last_price = 0;
}
if (productDetails.last_qu_factor_purchase_to_stock === null)
{
productDetails.last_qu_factor_purchase_to_stock = 1;
}
element.attr("data-product-details", event.productDetails);
var productOrderMissingButtonDisabledClasses = "disabled";
@ -205,7 +210,7 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + productDetails.last_price / productDetails.product.qu_factor_purchase_to_stock * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>';
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + productDetails.last_price / productDetails.last_qu_factor_purchase_to_stock * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>';
}
else
{

View File

@ -0,0 +1,65 @@
$('#save-barcode-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#barcode-form').serializeJSON();
Grocy.FrontendHelpers.BeginUiBusy("barcode-form");
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/product_barcodes', jsonData,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
else
{
Grocy.Api.Put('objects/product_barcodes/' + Grocy.EditObjectId, jsonData,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
window.parent.postMessage(WindowMessageBag("ProductBarcodesChanged"), U("/product/" + GetUriParam("product")));
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product")));
});
$('#barcode').on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('barcode-form');
});
$('#qu_factor_purchase_to_stock').on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('barcode-form');
});
$('#barcode-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('barcode-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-barcode-button').click();
}
}
});
Grocy.FrontendHelpers.ValidateForm('barcode-form');

View File

@ -42,6 +42,26 @@
function(result)
{
Grocy.EditObjectId = result.created_object_id;
if (prefillBarcode !== undefined)
{
var jsonDataBarcode = {};
jsonDataBarcode.barcode = prefillBarcode;
jsonDataBarcode.product_id = result.created_object_id;
jsonDataBarcode.qu_factor_purchase_to_stock = jsonData.qu_factor_purchase_to_stock;
jsonDataBarcode.shopping_location_id = jsonData.shopping_location_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
Grocy.Components.UserfieldsForm.Save(function()
{
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
@ -166,32 +186,19 @@
}
});
$('#barcode-taginput').tagsManager({
'hiddenTagListName': 'barcode',
'tagsContainer': '#barcode-taginput-container',
'tagClass': 'badge badge-secondary',
'delimiters': [13, 44]
});
if (Grocy.EditMode === 'edit')
{
Grocy.Api.Get('objects/products/' + Grocy.EditObjectId,
function (product)
Grocy.Api.Get('stock/products/' + Grocy.EditObjectId,
function(productDetails)
{
if (productDetails.last_purchased == null)
{
if (product.barcode !== null && product.barcode.length > 0)
{
product.barcode.split(',').forEach(function(item)
{
$('#barcode-taginput').tagsManager('pushTag', item);
});
}
},
function(xhr)
{
console.error(xhr);
$('#qu_id_stock').removeAttr("disabled");
}
);
}
},
function(xhr)
{
console.error(xhr);
}
);
var prefillName = GetUriParam('prefillname');
if (prefillName !== undefined)
@ -201,16 +208,6 @@ if (prefillName !== undefined)
}
var prefillBarcode = GetUriParam('prefillbarcode');
if (prefillBarcode !== undefined)
{
$('#barcode-taginput').tagsManager('pushTag', prefillBarcode);
$('#name').focus();
}
$("#barcode-taginput").on("blur", function(e)
{
$("#barcode-taginput").tagsManager("pushTag", $("#barcode-taginput").val());
});
$('.input-group-qu').on('change', function(e)
{
@ -249,12 +246,34 @@ $('#product-form input').keyup(function(event)
{
$("#qu-conversion-add-button").removeClass("disabled");
}
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#barcode-add-button").addClass("disabled");
}
else
{
if (prefillBarcode === undefined)
{
$("#barcode-add-button").removeClass("disabled");
}
}
});
$('#product-form select').change(function(event)
{
Grocy.FrontendHelpers.ValidateForm('product-form');
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#barcode-add-button").addClass("disabled");
}
else
{
if (prefillBarcode === undefined)
{
$("#barcode-add-button").removeClass("disabled");
}
}
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#qu-conversion-add-button").addClass("disabled");
@ -365,6 +384,17 @@ var quConversionsTable = $('#qu-conversions-table').DataTable({
$('#qu-conversions-table tbody').removeClass("d-none");
quConversionsTable.columns.adjust().draw();
var barcodeTable = $('#barcode-table').DataTable({
'order': [[1, 'asc']],
"orderFixed": [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
});
$('#barcode-table tbody').removeClass("d-none");
barcodeTable.columns.adjust().draw();
Grocy.Components.UserfieldsForm.Load();
$("#name").trigger("keyup");
$('#name').focus();
@ -412,17 +442,44 @@ $(document).on('click', '.qu-conversion-delete-button', function(e)
});
});
$(document).on('click', '.qu-conversion-edit-button', function (e)
$(document).on('click', '.barcode-delete-button', function(e)
{
var id = $(e.currentTarget).attr('data-qu-conversion-id');
Grocy.ProductEditFormRedirectUri = U("/quantityunitconversion/" + id.toString() + "?product=editobjectid");
$('#save-product-button').click();
});
var objectId = $(e.currentTarget).attr('data-barcode-id');
var productId = $(e.currentTarget).attr('data-product-id');
var barcode = $(e.currentTarget).attr('data-barcode');
var productBarcode = $(e.currentTarget).attr('data-product-barcode');
$("#qu-conversion-add-button").on("click", function(e)
{
Grocy.ProductEditFormRedirectUri = U("/quantityunitconversion/new?product=editobjectid");
$('#save-product-button').click();
bootbox.confirm({
message: __t('Are you sure to remove this barcode?'),
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/product_barcodes/' + objectId, { },
function(result)
{
Grocy.ProductEditFormRedirectUri = "reload";
$('#save-product-button').click();
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$('#qu_id_purchase').blur(function(e)
@ -436,12 +493,12 @@ $('#qu_id_purchase').blur(function(e)
}
});
$(document).on("Grocy.BarcodeScanned", function(e, barcode, target)
$(window).on("message", function(e)
{
if (target != "#barcode-taginput")
{
return;
}
var data = e.originalEvent.data;
$("#barcode-taginput").tagsManager("pushTag", barcode);
if (data.Message === "ProductBarcodesChanged" || data.Message === "ProductQUConversionChanged")
{
window.location.reload();
}
});

View File

@ -13,7 +13,7 @@
Grocy.EditObjectId = result.created_object_id;
Grocy.Components.UserfieldsForm.Save(function()
{
window.location.href = U('/productgroups');
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/productgroups"));
});
},
function(xhr)
@ -30,7 +30,7 @@
{
Grocy.Components.UserfieldsForm.Save(function()
{
window.location.href = U('/productgroups');
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/productgroups"));
});
},
function(xhr)

View File

@ -55,3 +55,12 @@ $(document).on('click', '.product-group-delete-button', function(e)
}
});
});
$(window).on("message", function(e)
{
var data = e.originalEvent.data;
if (data.Message === "CloseAllModals")
{
window.location.reload();
}
});

View File

@ -49,7 +49,7 @@ $(document).on('click', '.product-delete-button', function (e)
if (stockAmount.toString() == "0")
{
bootbox.confirm({
message: __t('Are you sure to delete product "%s"?', objectName),
message: __t('Are you sure you want to deactivate this product "%s"?', objectName),
closeButton: false,
buttons: {
confirm: {
@ -65,7 +65,9 @@ $(document).on('click', '.product-delete-button', function (e)
{
if (result === true)
{
Grocy.Api.Delete('objects/products/' + objectId, {},
jsonData = {};
jsonData.active = 0;
Grocy.Api.Put('objects/products/' + objectId, jsonData,
function (result)
{
window.location.href = U('/products');
@ -82,8 +84,8 @@ $(document).on('click', '.product-delete-button', function (e)
else
{
bootbox.alert({
title: __t('Delete not possible'),
message: __t('This product cannot be deleted because it is in stock, please remove the stock amount first.') + '<br><br>' + __t('Stock amount') + ': ' + stockAmount + ' ' + __n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural),
title: __t('Deactivation not possible'),
message: __t('This product cannot be deactivated because it is in stock, please remove the stock amount first.') + '<br><br>' + __t('Stock amount') + ': ' + stockAmount + ' ' + __n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural),
closeButton: false
});
}

View File

@ -8,12 +8,13 @@
Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails)
{
var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
var amount = jsonForm.amount * jsonForm.qu_factor_purchase_to_stock;
var price = "";
if (!jsonForm.price.toString().isEmpty())
{
price = parseFloat(jsonForm.price).toFixed(2);
// price is saved as 1 QU to stock
price = parseFloat(jsonForm.price / amount).toFixed(2);
if ($("input[name='price-type']:checked").val() == "total-price")
{
@ -37,6 +38,7 @@
{
jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
}
jsonData.qu_factor_purchase_to_stock = jsonForm.qu_factor_purchase_to_stock;
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
function(result)
@ -49,17 +51,13 @@
var addBarcode = GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
var jsonDataBarcode = {};
jsonDataBarcode.barcode = addBarcode;
jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.qu_factor_purchase_to_stock = jsonForm.qu_factor_purchase_to_stock;
jsonDataBarcode.shopping_location_id = jsonForm.shopping_location_id;
Grocy.Api.Put('objects/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result)
{
$("#flow-info-addbarcodetoselection").addClass("d-none");
@ -69,7 +67,7 @@
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form");
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
@ -140,13 +138,66 @@ if (Grocy.Components.ProductPicker !== undefined)
{
Grocy.Components.ProductCard.Refresh(productId);
if (document.getElementById("product_id").getAttribute("barcode") != "null")
{
Grocy.Api.Get('productbarcodedetails/' + document.getElementById("product_id").getAttribute("barcode"),
function(resultBarcode)
{
if (resultBarcode != null)
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", resultBarcode.qu_factor_purchase_to_stock);
$('#product_id').attr("barcode-shopping-location-id", resultBarcode.shopping_location_id);
}
else
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
$('#product_id').attr("barcode-shopping-location-id", "null");
}
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
$('#product_id').attr("barcode-shopping-location-id", "null");
}
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
{
$('#price').val(parseFloat(productDetails.last_price).toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 }));
var qu_factor_purchase_to_stock = null;
var barcode_shopping_location_id = null;
if (document.getElementById("product_id").getAttribute("barcode") != "null" && document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock") != "null")
{
qu_factor_purchase_to_stock = document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock");
barcode_shopping_location_id = document.getElementById("product_id").getAttribute("barcode-shopping-location-id");
}
else
{
if (productDetails.last_qu_factor_purchase_to_stock != null)
{
qu_factor_purchase_to_stock = productDetails.last_qu_factor_purchase_to_stock;
}
else
{
qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
}
}
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
if (productDetails.last_shopping_location_id != null)
if (barcode_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(barcode_shopping_location_id);
}
else if (productDetails.last_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
}
@ -161,15 +212,22 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
$('#amount_qu_unit').attr("qu-factor-purchase-to-stock", productDetails.product.qu_factor_purchase_to_stock);
$('#amount_qu_unit').attr("qu-factor-purchase-to-stock", qu_factor_purchase_to_stock);
$('#amount_qu_unit').attr("quantity-unit-purchase-name", productDetails.quantity_unit_purchase.name);
$('#amount_qu_unit').attr("quantity-unit-stock-name", productDetails.quantity_unit_stock.name);
if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock)
$('#amount_qu_unit').attr("quantity-unit-stock-name-plural", productDetails.quantity_unit_stock.name_plural);
$('#qu_factor_purchase_to_stock').val(qu_factor_purchase_to_stock);
if (qu_factor_purchase_to_stock == 1)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
$('#group-qu_factor_purchase_to_stock').addClass('d-none');
}
else
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(productDetails.product.qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
$('#group-qu_factor_purchase_to_stock').removeClass('d-none');
}
var priceTypeUnitPrice = $("#price-type-unit-price");
@ -193,7 +251,7 @@ if (Grocy.Components.ProductPicker !== undefined)
if (productDetails.product.enable_tare_weight_handling == 1)
{
var minAmount = parseFloat(productDetails.product.tare_weight) / productDetails.product.qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
var minAmount = parseFloat(productDetails.product.tare_weight) / qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
$("#amount").attr("min", minAmount);
$("#amount").attr("step", "0.0001");
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString()));
@ -339,6 +397,15 @@ $('#amount').on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('purchase-form');
});
$('#qu_factor_purchase_to_stock').on('change', function(e)
{
var value = $(e.target).val();
$('#amount_qu_unit').attr("qu-factor-purchase-to-stock", value);
$('#amount_qu_unit').text(document.getElementById("amount_qu_unit").getAttribute("quantity-unit-purchase-name") + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(value).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, document.getElementById("amount_qu_unit").getAttribute("quantity-unit-stock-name"), document.getElementById("amount_qu_unit").getAttribute("quantity-unit-stock-name-plural")) + ")"));
refreshPriceHint();
Grocy.FrontendHelpers.ValidateForm('purchase-form');
});
if (GetUriParam("flow") === "shoppinglistitemtostock")
{
$('#amount').val(parseFloat(GetUriParam("amount")).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 4 }));

View File

@ -25,7 +25,8 @@
}
else
{
window.location.href = U("/product/" + GetUriParam("product"));
window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product")));
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product")));
}
});
},
@ -54,7 +55,8 @@
}
else
{
window.location.href = U("/product/" + GetUriParam("product"));
window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product")));
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product")));
}
});
},
@ -79,7 +81,8 @@
}
else
{
window.location.href = U("/product/" + GetUriParam("product"));
window.parent.postMessage(WindowMessageBag("ProductQUConversionChanged"), U("/product/" + GetUriParam("product")));
window.parent.postMessage(WindowMessageBag("CloseAllModals"), U("/product/" + GetUriParam("product")));
}
});
},

View File

@ -177,6 +177,7 @@ function RefreshStockEntryRow(stockRowId)
);
$('#stock-' + stockRowId + '-price').text(result.price);
$('#stock-' + stockRowId + '-qu-factor-purchase-to-stock').text(result.qu_factor_purchase_to_stock);
$('#stock-' + stockRowId + '-purchased-date').text(result.purchased_date);
$('#stock-' + stockRowId + '-purchased-date-timeago').attr('datetime', result.purchased_date + ' 23:59:59');

View File

@ -26,6 +26,9 @@
jsonData.location_id = 1;
}
jsonData.price = price;
jsonData.qu_factor_purchase_to_stock = jsonForm.qu_factor_purchase_to_stock;
console.log(jsonForm);
console.log(jsonData);
jsonData.open = $("#open").is(":checked");

View File

@ -1,9 +1,10 @@
var stockOverviewTable = $('#stock-overview-table').DataTable({
'order': [[3, 'asc']],
'order': [[4, 'asc']],
'colReorder': false,
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 4 },
{ 'searchable': false, "targets": 0 },
{ 'visible': false, 'targets': 5 },
{ 'visible': false, 'targets': 6 }
],
@ -19,7 +20,7 @@ $("#location-filter").on("change", function()
value = "";
}
stockOverviewTable.column(4).search(value).draw();
stockOverviewTable.column(5).search(value).draw();
});
$("#product-group-filter").on("change", function()
@ -30,7 +31,7 @@ $("#product-group-filter").on("change", function()
value = "";
}
stockOverviewTable.column(6).search(value).draw();
stockOverviewTable.column(2).search(value).draw();
});
$("#status-filter").on("change", function()
@ -44,7 +45,7 @@ $("#status-filter").on("change", function()
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
stockOverviewTable.column(5).search(value).draw();
stockOverviewTable.column(6).search(value).draw();
});
$(".status-filter-message").on("click", function()
@ -252,7 +253,7 @@ function RefreshProductRow(productId)
$('#product-' + productId + '-qu-name').text(__n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural));
$('#product-' + productId + '-amount').text(result.stock_amount);
$('#product-' + productId + '-consume-all-button').attr('data-consume-amount', result.stock_amount);
$('#product-' + productId + '-factor-purchase-amount').text(__t('( %s', result.stock_factor_purchase_amount));
$('#product-' + productId + '-next-best-before-date').text(result.next_best_before_date);
$('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date);

View File

@ -30,17 +30,12 @@
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
var jsonDataBarcode = {};
jsonDataBarcode.barcode = addBarcode;
jsonDataBarcode.product_id = jsonForm.product_id;
jsonDataBarcode.qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
Grocy.Api.Put('objects/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result)
{
$("#flow-info-addbarcodetoselection").addClass("d-none");
@ -49,7 +44,8 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}

View File

@ -45,6 +45,7 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/stockentry/{entryId}', '\Grocy\Controllers\StockController:StockEntryEditForm');
$group->get('/products', '\Grocy\Controllers\StockController:ProductsList');
$group->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
$group->get('/productbarcodes/{productBarcodeId}', '\Grocy\Controllers\StockController:ProductBarcodesEditForm');
$group->get('/stocksettings', '\Grocy\Controllers\StockController:StockSettings');
$group->get('/locations', '\Grocy\Controllers\StockController:LocationsList');
$group->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
@ -199,6 +200,7 @@ $app->group('/api', function(RouteCollectorProxy $group)
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
$group->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
$group->get('/productbarcodedetails/{barcode}', '\Grocy\Controllers\StockApiController:ProductBarcodeDetails');
}
// Shopping list

View File

@ -96,7 +96,7 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Milk')}', 2, 10, 10, 1, 6, 1); --23
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Milk Chocolate')}', 4, 3, 3, 1, 1, 2); --24
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Dark Chocolate')}', 4, 3, 3, 1, 1, 2); --25
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, barcode) VALUES ('{$this->__t_sql('Waffle rolls')}', 4, 3, 3, 1, 1, '22111289'); --26
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Waffle rolls')}', 4, 3, 3, 1, 1); --26
UPDATE products SET calories = 123 WHERE IFNULL(calories, 0) = 0;
/* Prevent invalid quantity unit assignments */
@ -189,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(), 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->AddProduct(3, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 10, null, 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(), 10, null, 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(), 10, null, 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(), 10, null, 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(), 10, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, 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(), 1.0, null, null, $this->NextSupermarketId());
$stockService->AddMissingProductsToShoppingList();
$stockService->OpenProduct(3, 1);
$stockService->OpenProduct(6, 1);

View File

@ -25,7 +25,7 @@ class StockService extends BaseService
$missingProductsView = 'stock_missing_products';
}
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)';
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)';
}
$currentStockMapped = $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_GROUP|\PDO::FETCH_OBJ);
@ -51,12 +51,6 @@ class StockService extends BaseService
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetCurrentProductPrices()
{
$sql = 'SELECT * FROM products_current_price';
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetMissingProducts()
{
$sql = 'SELECT * FROM stock_missing_products_including_opened';
@ -75,7 +69,7 @@ class StockService extends BaseService
public function GetProductIdFromBarcode(string $barcode)
{
$potentialProduct = $this->getDatabase()->products()->where("',' || barcode || ',' LIKE '%,' || :1 || ',%' AND IFNULL(barcode, '') != ''", $barcode)->limit(1)->fetch();
$potentialProduct = $this->getDatabase()->product_barcodes()->where("barcode = :1", $barcode)->fetch();
if ($potentialProduct === null)
{
@ -102,7 +96,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
$stockCurrentRow = FindObjectinArrayByPropertyValue($this->GetCurrentStock(), 'product_id', $productId);
@ -111,6 +105,8 @@ class StockService extends BaseService
{
$stockCurrentRow = new \stdClass();
$stockCurrentRow->amount = 0;
$stockCurrentRow->factor_purchase_amount = 0;
$stockCurrentRow->value = 0;
$stockCurrentRow->amount_opened = 0;
$stockCurrentRow->amount_aggregated = 0;
$stockCurrentRow->amount_opened_aggregated = 0;
@ -118,23 +114,20 @@ class StockService extends BaseService
}
$product = $this->getDatabase()->products($productId);
$productLastPurchased = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->where('undone', 0)->max('purchased_date');
$productBarcodes = $this->getDatabase()->product_barcodes()->where('product_id', $productId)->fetchAll();
$productLastPurchased = $this->getDatabase()->products_last_purchased()->where('product_id', $productId)->fetch();
$productLastUsed = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date');
$nextBestBeforeDate = $this->getDatabase()->stock()->where('product_id', $productId)->min('best_before_date');
$quPurchase = $this->getDatabase()->quantity_units($product->qu_id_purchase);
$quStock = $this->getDatabase()->quantity_units($product->qu_id_stock);
$location = $this->getDatabase()->locations($product->location_id);
$averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days);
$lastPrice = null;
$lastPrice = $productLastPurchased->price;
$lastQuFactorPurchaseToStock = $productLastPurchased->qu_factor_purchase_to_stock;
$avgPrice = $this->getDatabase()->products_average_price()->where('product_id', $productId)->fetch();
$oldestPrice = $this->getDatabase()->products_oldest_stock_unit_price()->where('product_id', $productId)->fetch();
$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;
}
$lastShoppingLocation = $productLastPurchased->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;
$consumeCountSpoiled = $this->getDatabase()->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 1')->sum('amount') * -1;
@ -146,15 +139,21 @@ class StockService extends BaseService
return array(
'product' => $product,
'last_purchased' => $productLastPurchased,
'product_barcodes' => $productBarcodes,
'last_purchased' => $productLastPurchased->purchased_date,
'last_used' => $productLastUsed,
'stock_amount' => $stockCurrentRow->amount,
'stock_factor_purchase_amount' => $stockCurrentRow->factor_purchase_amount,
'stock_value' => $stockCurrentRow->value,
'stock_amount_opened' => $stockCurrentRow->amount_opened,
'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated,
'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated,
'quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
'last_qu_factor_purchase_to_stock' => $lastQuFactorPurchaseToStock,
'avg_price' => $avgPrice->price,
'oldest_price' => $oldestPrice->price,
'last_shopping_location_id' => $lastShoppingLocation,
'default_shopping_location_id' => $product->shopping_location_id,
'next_best_before_date' => $nextBestBeforeDate,
@ -169,7 +168,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
$returnData = array();
@ -217,11 +216,11 @@ class StockService extends BaseService
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId);
}
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null)
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $quFactorPurchaseToStock, $locationId = null, $shoppingLocationId = null, &$transactionId = null)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
// Tare weight handling
@ -275,6 +274,7 @@ class StockService extends BaseService
'location_id' => $locationId,
'transaction_id' => $transactionId,
'shopping_location_id' => $shoppingLocationId,
'qu_factor_purchase_to_stock' => $quFactorPurchaseToStock,
));
$logRow->save();
@ -289,6 +289,7 @@ class StockService extends BaseService
'price' => $price,
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'qu_factor_purchase_to_stock' => $quFactorPurchaseToStock,
));
$stockRow->save();
@ -304,7 +305,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
if ($locationId !== null && !$this->LocationExists($locationId))
@ -423,7 +424,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
if (!$this->LocationExists($locationIdFrom))
@ -509,6 +510,7 @@ class StockService extends BaseService
'stock_id' => $stockEntry->stock_id,
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM,
'price' => $stockEntry->price,
'qu_factor_purchase_to_stock' => $stockEntry->qu_factor_purchase_to_stock,
'opened_date' => $stockEntry->opened_date,
'location_id' => $stockEntry->location_id,
'correlation_id' => $correlationId,
@ -524,6 +526,7 @@ class StockService extends BaseService
'stock_id' => $stockEntry->stock_id,
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO,
'price' => $stockEntry->price,
'qu_factor_purchase_to_stock' => $stockEntry->qu_factor_purchase_to_stock,
'opened_date' => $stockEntry->opened_date,
'location_id' => $locationIdTo,
'correlation_id' => $correlationId,
@ -550,6 +553,7 @@ class StockService extends BaseService
'stock_id' => $stockEntry->stock_id,
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM,
'price' => $stockEntry->price,
'qu_factor_purchase_to_stock' => $stockEntry->qu_factor_purchase_to_stock,
'opened_date' => $stockEntry->opened_date,
'location_id' => $stockEntry->location_id,
'correlation_id' => $correlationId,
@ -565,6 +569,7 @@ class StockService extends BaseService
'stock_id' => $stockEntry->stock_id,
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO,
'price' => $stockEntry->price,
'qu_factor_purchase_to_stock' => $stockEntry->qu_factor_purchase_to_stock,
'opened_date' => $stockEntry->opened_date,
'location_id' => $locationIdTo,
'correlation_id' => $correlationId,
@ -584,6 +589,7 @@ class StockService extends BaseService
'best_before_date' => $newBestBeforeDate,
'purchased_date' => $stockEntry->purchased_date,
'stock_id' => $stockEntry->stock_id,
'qu_factor_purchase_to_stock' => $stockEntry->qu_factor_purchase_to_stock,
'price' => $stockEntry->price,
'location_id' => $locationIdTo,
'open' => $stockEntry->open,
@ -598,7 +604,7 @@ class StockService extends BaseService
return $this->getDatabase()->lastInsertId();
}
public function EditStockEntry(int $stockRowId, float $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate)
public function EditStockEntry(int $stockRowId, float $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate, $quFactorPurchaseToStock)
{
$stockRow = $this->getDatabase()->stock()->where('id = :1', $stockRowId)->fetch();
@ -621,6 +627,7 @@ class StockService extends BaseService
'opened_date' => $stockRow->opened_date,
'location_id' => $stockRow->location_id,
'shopping_location_id' => $stockRow->shopping_location_id,
'qu_factor_purchase_to_stock' => $stockRow->qu_factor_purchase_to_stock,
'correlation_id' => $correlationId,
'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id
@ -645,6 +652,7 @@ class StockService extends BaseService
'shopping_location_id' => $shoppingLocationId,
'opened_date' => $openedDate,
'open' => $open,
'qu_factor_purchase_to_stock' => $quFactorPurchaseToStock,
'purchased_date' => $purchasedDate
));
@ -659,6 +667,7 @@ class StockService extends BaseService
'opened_date' => $stockRow->opened_date,
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'qu_factor_purchase_to_stock' => $stockRow->qu_factor_purchase_to_stock,
'correlation_id' => $correlationId,
'transaction_id' => $transactionId,
'stock_row_id' => $stockRow->id
@ -672,7 +681,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object)$this->GetProductDetails($productId);
@ -728,7 +737,7 @@ class StockService extends BaseService
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
$productStockAmountUnopened = $this->getDatabase()->stock()->where('product_id = :1 AND open = 0', $productId)->sum('amount');
@ -908,7 +917,7 @@ class StockService extends BaseService
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
throw new \Exception('Product does not exist or is inactive');
}
$alreadyExistingEntry = $this->getDatabase()->shopping_list()->where('product_id = :1 AND shopping_list_id = :2', $productId, $listId)->fetch();
@ -934,7 +943,7 @@ class StockService extends BaseService
private function ProductExists($productId)
{
$productRow = $this->getDatabase()->products()->where('id = :1', $productId)->fetch();
$productRow = $this->getDatabase()->products()->where('id = :1 and active = 1', $productId)->fetch();
return $productRow !== null;
}
@ -1145,6 +1154,7 @@ class StockService extends BaseService
'best_before_date' => $logRow->best_before_date,
'purchased_date' => $logRow->purchased_date,
'price' => $logRow->price,
'qu_factor_purchase_to_stock' => $logRow->qu_factor_purchase_to_stock,
'location_id' => $logRow->location_id,
'open' => $open,
'opened_date' => $openedDate

View File

@ -16,7 +16,7 @@
@php if(!isset($isRequired)) { $isRequired = true; } @endphp
@php if(!isset($noNameAttribute)) { $noNameAttribute = false; } @endphp
<div class="form-group {{ $additionalGroupCssClasses }}">
<div id="group-{{ $id }}" class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">
{{ $__t($label) }}&nbsp;
<i class="fas fa-question-circle" id="{{ $hintId }}" data-toggle="tooltip" title="{{ $hint }}"></i>{!! $additionalHtmlContextHelp !!}</label>

View File

@ -21,12 +21,16 @@
<a class="collapsed" data-toggle="collapse" href="#productcard-product-description">{{ $__t('Show more') }}</a>
</div>
<strong>{{ $__t('Stock amount') . ' / ' . $__t('Quantity unit') }}:</strong> <span id="productcard-product-stock-amount" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name"></span> <span id="productcard-product-stock-opened-amount" class="small font-italic locale-number locale-number-quantity-amount"></span>
<strong>{{ $__t('Stock amount') }}:</strong> <span id="productcard-product-stock-amount" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name"></span>
<span id="productcard-product-stock-factor-purchase-amount" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-factor-purchase-qu-name"></span>
<span id="productcard-product-stock-opened-amount" class="small font-italic locale-number locale-number-quantity-amount"></span><br>
<strong>{{ $__t('Stock value') }}:</strong> <span id="productcard-product-stock-value" class="locale-number locale-number-currency"></span>
<span id="productcard-aggregated-amounts" class="pl-2 text-secondary d-none"><i class="fas fa-custom-sigma-sign"></i> <span id="productcard-product-stock-amount-aggregated" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name-aggregated"></span> <span id="productcard-product-stock-opened-amount-aggregated locale-number locale-number-quantity-amount" class="small font-italic"></span></span><br>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<strong>{{ $__t('Default location') }}:</strong> <span id="productcard-product-location"></span><br>@endif
<strong>{{ $__t('Last purchased') }}:</strong> <span id="productcard-product-last-purchased"></span> <time id="productcard-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br>@endif
@if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Average price') }}:</strong> <span id="productcard-product-average-price"></span><br>@endif
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)<strong>{{ $__t('Average shelf life') }}:</strong> <span id="productcard-product-average-shelf-life"></span><br>@endif
<strong>{{ $__t('Spoil rate') }}:</strong> <span id="productcard-product-spoil-rate"></span>

View File

@ -21,7 +21,7 @@
<select class="form-control product-combobox barcodescanner-input" id="product_id" name="product_id" @if($isRequired) required @endif @if($disabled) disabled @endif data-target="@productpicker">
<option value=""></option>
@foreach($products as $product)
<option data-additional-searchdata="{{ $product->barcode }}@if(!empty($product->barcode)),@endif" value="{{ $product->id }}">{{ $product->name }}</option>
<option data-additional-searchdata="{{ FindObjectInArrayByPropertyValue($barcodes, 'product_id', $product->id)->barcodes }}," value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('You have to select a product') }}</div>

View File

@ -30,6 +30,7 @@
@include('components.productpicker', array(
'products' => $products,
'barcodes' => $barcodes,
'nextInputSelector' => '#amount',
'disallowAddProductWorkflows' => true
))

View File

@ -13,6 +13,7 @@
@include('components.productpicker', array(
'products' => $products,
'barcodes' => $barcodes,
'nextInputSelector' => '#new_amount'
))

View File

@ -0,0 +1,71 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $__t('Edit Barcode'))
@else
@section('title', $__t('Create Barcode'))
@endif
@section('viewJsName', 'productbarcodesform')
@section('content')
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
<hr>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-xs-12">
<h3 class="text-muted">{{ $__t('Barcode for product') }} <strong>{{ $product->name }}</strong></h3>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $barcode->id }};</script>
@endif
<form id="barcode-form" novalidate>
<input type="hidden" name="product_id" value="{{ $product->id }}">
<div class="form-group">
<label for="name">{{ $__t('Barcode') }}<i class="fas fa-barcode"></i></label>
<div class="input-group">
<input type="text" class="form-control barcodescanner-input" required id="barcode" name="barcode" value="@if($mode == 'edit'){{ $barcode->barcode }}@endif" data-target="#scanned_barcode">
@include('components.barcodescanner')
</div>
</div>
@php if($mode == 'edit') { $value = $barcode->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp
@include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock',
'label' => 'Factor purchase to stock quantity unit',
'min' => 1,
'value' => $value,
'isRequired' => true,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
'additionalCssClasses' => 'input-group-qu',
))
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="form-group">
<label for="shopping_location_id_id">{{ $__t('Default store') }}</label>
<select class="form-control" id="shopping_location_id" name="shopping_location_id">
<option></option>
@foreach($shoppinglocations as $store)
<option @if($mode == 'edit' && $store->id == $product->shopping_location_id) selected="selected" @endif value="{{ $store->id }}">{{ $store->name }}</option>
@endforeach
</select>
</div>
@else
<input type="hidden" name="shopping_location_id" id="shopping_location_id" value="1">
@endif
<button id="save-barcode-button" class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@ -46,6 +46,14 @@
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="hidden" name="active" value="1">
<input @if($mode == 'create') checked @elseif($mode == 'edit' && $product->active == 1) checked @endif class="form-check-input" type="checkbox" id="active" name="active" value="1">
<label class="form-check-label" for="active">{{ $__t('Active') }}</label>
</div>
</div>
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->parent_product_id; } @endphp
@php
$hint = '';
@ -56,7 +64,6 @@
@endphp
@include('components.productpicker', array(
'products' => $products,
'nextInputSelector' => '#barcode-taginput',
'prefillById' => $prefillById,
'disallowAllProductWorkflows' => true,
'isRequired' => false,
@ -70,14 +77,6 @@
<textarea class="form-control wysiwyg-editor" id="description" name="description">@if($mode == 'edit'){{ $product->description }}@endif</textarea>
</div>
<div class="form-group tm-group">
<label for="barcode-taginput">{{ $__t('Barcode(s)') }}&nbsp;&nbsp;<i class="fas fa-barcode"></i></label>
<div class="input-group">
<input type="text" class="form-control tm-input barcodescanner-input" id="barcode-taginput" data-target="#barcode-taginput">
</div>
<div id="barcode-taginput-container"></div>
</div>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group">
<label for="location_id">{{ $__t('Default location') }}</label>
@ -171,7 +170,8 @@
<div class="form-group">
<label for="qu_id_stock">{{ $__t('Quantity unit stock') }}</label>
<select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock">
<i class="fas fa-question-circle" data-toggle="tooltip" title="Quantity unit stock cannot be changed after first purchase"></i>
<select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock" @if($mode == 'edit') disabled @endif>
<option></option>
@foreach($quantityunits as $quantityunit)
<option @if($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) selected="selected" @endif value="{{ $quantityunit->id }}" data-plural-form="{{ $quantityunit->name_plural }}">{{ $quantityunit->name }}</option>
@ -295,7 +295,7 @@
<div class="col-lg-6 col-xs-12">
<h2>
{{ $__t('QU conversions') }}
<a id="qu-conversion-add-button" class="btn btn-outline-dark" href="#">
<a class="btn btn-outline-dark show-as-dialog-link" type="button" href="{{ $U('/quantityunitconversion/new?embedded&product=' . $product->id ) }}">
<i class="fas fa-plus"></i> {{ $__t('Add') }}
</a>
</h2>
@ -316,7 +316,7 @@
@if($quConversion->product_id == $product->id || $quConversion->product_id == null)
<tr>
<td class="fit-content border-right">
<a class="btn btn-sm btn-info qu-conversion-edit-button @if($quConversion->product_id == null) disabled @endif" href="#" data-qu-conversion-id="{{ $quConversion->id }}">
<a class="btn btn-sm btn-info show-as-dialog-link @if($quConversion->product_id == null) disabled @endif" href="{{ $U('/quantityunitconversion/' . $quConversion->id . '?embedded&product=' . $product->id ) }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-sm btn-danger qu-conversion-delete-button @if($quConversion->product_id == null) disabled @endif" href="#" data-qu-conversion-id="{{ $quConversion->id }}">
@ -346,6 +346,55 @@
</tbody>
</table>
<h2>
{{ $__t('Barcodes') }}
<a class="btn btn-outline-dark show-as-dialog-link" type="button" href="{{ $U('/productbarcodes/new?embedded&product=' . $product->id ) }}">
<i class="fas fa-plus"></i> {{ $__t('Add') }}
</a>
</h2>
<h5 id="barcode-headline-info" class="text-muted font-italic"></h5>
<table id="barcode-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $__t('Barcode') }}</th>
<th>{{ $__t('Factor purchase to stock quantity unit') }}</th>
<th>{{ $__t('Store') }}</th>
</tr>
</thead>
<tbody class="d-none">
@if($mode == "edit")
@foreach($barcodes as $barcode)
@if($barcode->product_id == $product->id || $barcode->product_id == null)
<tr>
<td class="fit-content border-right">
<a class="btn btn-sm btn-info show-as-dialog-link @if($barcode->product_id == null) disabled @endif" href="{{ $U('/productbarcodes/' . $barcode->id . '?embedded&product=' . $product->id ) }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-sm btn-danger barcode-delete-button @if($barcode->product_id == null) disabled @endif" href="#" data-barcode-id="{{ $barcode->id }}" data-barcode="{{ $barcode->barcode }}" data-product-barcode="{{ $product->barcode }}" data-product-id="{{ $product->id }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $barcode->barcode }}
</td>
<td>
<span class="locale-number locale-number-quantity-amount">{{ $barcode->qu_factor_purchase_to_stock }}</span>
</td>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<td id="barcode-shopping-location">
@if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id) !== null)
{{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id)->name }}
@endif
</td>
@endif
</tr>
@endif
@endforeach
@endif
</tbody>
</table>
<div class="pt-5">
<label class="mt-2">{{ $__t('Picture') }}</label>
<button id="delete-current-product-picture-button" class="btn btn-sm btn-danger @if(empty($product->picture_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $__t('Delete') }}</button>

View File

@ -20,7 +20,7 @@
<hr>
<div class="row mt-3">
<div class="col-xs-12 col-md-2 col-xl-1">
<a class="btn btn-primary btn-sm responsive-button w-100 mb-3" href="{{ $U('/productgroup/new') }}">
<a class="btn btn-primary btn-sm responsive-button w-100 mb-3 show-as-dialog-link" href="{{ $U('/productgroup/new?embedded') }}">
{{ $__t('Add') }}
</a>
</div>
@ -55,7 +55,7 @@
@foreach($productGroups as $productGroup)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/productgroup/') }}{{ $productGroup->id }}">
<a class="btn btn-info btn-sm show-as-dialog-link" href="{{ $U('/productgroup/') }}{{ $productGroup->id }}?embedded">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm product-group-delete-button" href="#" data-group-id="{{ $productGroup->id }}" data-group-name="{{ $productGroup->name }}">

View File

@ -81,12 +81,12 @@
<a class="btn btn-info btn-sm" href="{{ $U('/product/') }}{{ $product->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm product-delete-button" href="#" data-product-id="{{ $product->id }}" data-product-name="{{ $product->name }}">
<a class="btn btn-danger btn-sm product-delete-button @if($product->active == 0) disabled @endif" href="#" data-product-id="{{ $product->id }}" data-product-name="{{ $product->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
@if($product->active == 0) (deactivated) @endif {{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td>
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}

View File

@ -30,6 +30,7 @@
@include('components.productpicker', array(
'products' => $products,
'barcodes' => $barcodes,
'nextInputSelector' => '#amount'
))
@ -96,6 +97,16 @@
<input type="hidden" name="price" id="price" value="0">
@endif
@include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock',
'label' => 'Factor purchase to stock quantity unit',
'min' => 1,
'additionalGroupCssClasses' => 'd-none',
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
'additionalCssClasses' => 'input-group-qu',
'additionalHtmlElements' => '<p id="qu-conversion-info" class="form-text text-muted small d-none"></p>'
))
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@include('components.locationpicker', array(
'locations' => $locations,

View File

@ -134,7 +134,7 @@
<h5 class="card-title mb-1">{{ $recipe->name }}</h5>
<p class="card-text">
@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1){{ $__t('Enough in stock') }}@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1){{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing but already on the shopping list', 'Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing', 'Not enough in stock, %s ingredients missing') }}@endif</span>
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1){{ $__t('Enough in stock') }}@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1){{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock, %s ingredient missing but already on the shopping list', 'Not enough in stock, %s ingredients missing but already on the shopping list') }}@else{{ $__n(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->missing_products_count, 'Not enough in stock (not included in costs), %s ingredient missing', 'Not enough in stock (not included in costs), %s ingredients missing') }}@endif</span>
</p>
</div>
</div>
@ -250,7 +250,7 @@
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="col-5">
<label>{{ $__t('Costs') }}&nbsp;</label>
<i class="fas fa-question-circle" data-toggle="tooltip" title="{{ $__t('Based on the prices of the last purchase per product') }}"></i>
<i class="fas fa-question-circle" data-toggle="tooltip" title="{{ $__t('Based on the prices of the default consume rule which is "First expiring first, then first in first out"') }}"></i>
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3>
</div>
@endif
@ -310,6 +310,9 @@
<h6 class="mb-2 mt-2 @if($hasIngredientGroups) ml-3 @else ml-1 @endif"><strong>{{ $selectedRecipePosition->product_group }}</strong></h6>
@endif
<li class="list-group-item px-0 @if($hasIngredientGroups && $hasProductGroups) ml-4 @elseif($hasIngredientGroups || $hasProductGroups) ml-2 @else ml-0 @endif">
@if($selectedRecipePosition->product_active == 0)
<div class="small text-muted font-italic">{{ $__t('Deactivated Product') }}</div>
@endif
@php
$product = FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id);
$productQuConversions = FindAllObjectsInArrayByPropertyValue($quantityUnitConversionsResolved, 'product_id', $product->id);
@ -327,7 +330,7 @@
@endif
{{ $__n($selectedRecipePosition->recipe_amount, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityUnits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }}
@if($selectedRecipePosition->need_fulfilled == 1)<i class="fas fa-check text-success"></i>@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)<i class="fas fa-exclamation text-warning"></i>@else<i class="fas fa-times text-danger"></i>@endif
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock, %1$s missing, %2$s already on shopping list', round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, 2), round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list, 2)) }} @endif</span>
<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $__t('Enough in stock') }} @else {{ $__t('Not enough in stock (not included in costs), %1$s missing, %2$s already on shopping list', round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, 2), round(FindObjectInArrayByPropertyValue($recipePositionsResolved, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list, 2)) }} @endif</span>
@if(!empty($selectedRecipePosition->recipe_variable_amount))
<div class="small text-muted font-italic">{{ $__t('Variable amount') }}</div>

View File

@ -41,6 +41,7 @@
<th>{{ $__t('Store') }}</th>
<th>{{ $__t('Price') }}</th>
@endif
<th>{{ $__t('Factor purchase to stock') }}</th>
<th>{{ $__t('Purchased date') }}</th>
@include('components.userfields_thead', array(
@ -158,6 +159,9 @@
{{ $stockEntry->price }}
</td>
@endif
<td id="stock-{{ $stockEntry->id }}-qu-factor-purchase-to-stock">
{{ $stockEntry->qu_factor_purchase_to_stock }}
</td>
<td>
<span id="stock-{{ $stockEntry->id }}-purchased-date">{{ $stockEntry->purchased_date }}</span>
<time id="stock-{{ $stockEntry->id }}-purchased-date-timeago" class="timeago timeago-contextual" datetime="{{ $stockEntry->purchased_date }} 23:59:59"></time>

View File

@ -60,6 +60,16 @@
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-small text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
))
@include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock',
'label' => 'Factor purchase to stock quantity unit',
'value' => $stockEntry->qu_factor_purchase_to_stock,
'min' => 1,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
'additionalCssClasses' => 'input-group-qu',
'additionalHtmlElements' => '<p id="qu-conversion-info" class="form-text text-muted small d-none"></p>'
))
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@include('components.numberpicker', array(
'id' => 'price',

View File

@ -104,11 +104,11 @@
<tr>
<th class="border-right"></th>
<th>{{ $__t('Product') }}</th>
<th>{{ $__t('Product group') }}</th>
<th>{{ $__t('Amount') }}</th>
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) d-none @endif">{{ $__t('Next best before date') }}</th>
<th class="d-none">Hidden location</th>
<th class="d-none">Hidden status</th>
<th class="d-none">Hidden product group</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
@ -206,9 +206,16 @@
<td class="product-name-cell cursor-link" data-product-id="{{ $currentStockEntry->product_id }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
</td>
@php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp
<td>
@if($productGroup !== null){{ $productGroup->name }}@endif
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}</span>
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount" class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
@if($currentStockEntry->amount != $currentStockEntry->factor_purchase_amount)
<span id="product-{{ $currentStockEntry->product_id }}-factor-purchase-amount" class="locale-number locale-number-quantity-amount">({{ $currentStockEntry->factor_purchase_amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-purchase-name">{{ $__n($currentStockEntry->factor_purchase_amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_purchase)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_purchase)->name_plural) }})</span>
@endif
@if($currentStockEntry->is_aggregated_amount == 1)
<span class="pl-1 text-secondary">
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
@ -236,10 +243,6 @@
<td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @endif @if(FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
</td>
@php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp
<td class="d-none">
@if($productGroup !== null){{ $productGroup->name }}@endif
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,

View File

@ -14,6 +14,7 @@
@include('components.productpicker', array(
'products' => $products,
'barcodes' => $barcodes,
'nextInputSelector' => '#location_id_from',
'disallowAddProductWorkflows' => true
))