diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 5db2038b..3eb360a5 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -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) diff --git a/controllers/StockController.php b/controllers/StockController.php index 693bd016..f704c84e 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -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; diff --git a/grocy.openapi.json b/grocy.openapi.json index c19a0c6b..20af6b9f 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -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": { diff --git a/localization/strings.pot b/localization/strings.pot index aced7907..3951c972 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -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 "" diff --git a/migrations/0103.sql b/migrations/0103.sql new file mode 100644 index 00000000..862d01f5 --- /dev/null +++ b/migrations/0103.sql @@ -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; diff --git a/migrations/0104.sql b/migrations/0104.sql new file mode 100644 index 00000000..edb883eb --- /dev/null +++ b/migrations/0104.sql @@ -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; diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 9b6c3ba4..2fc953aa 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -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"); diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js index 15649c85..abbd5589 100644 --- a/public/viewjs/components/productpicker.js +++ b/public/viewjs/components/productpicker.js @@ -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'); } diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 1661b955..eb6572cf 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -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); } ); } diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index df4727a5..9f173f5d 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -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); } ); } diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index b1dedbc7..9570cda5 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -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 = '
' + productDetails.last_price / productDetails.product.qu_factor_purchase_to_stock * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '
'; + costsAndCaloriesPerServing = '
' + productDetails.last_price / productDetails.last_qu_factor_purchase_to_stock * mealPlanEntry.product_amount + ' / ' + productDetails.product.calories * mealPlanEntry.product_amount + ' kcal ' + '
'; } else { diff --git a/public/viewjs/productbarcodesform.js b/public/viewjs/productbarcodesform.js new file mode 100644 index 00000000..29b57822 --- /dev/null +++ b/public/viewjs/productbarcodesform.js @@ -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'); diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index 4e4edaa6..3761b2f5 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -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") + var data = e.originalEvent.data; + + if (data.Message === "ProductBarcodesChanged" || data.Message === "ProductQUConversionChanged") { - return; + window.location.reload(); } - - $("#barcode-taginput").tagsManager("pushTag", barcode); }); diff --git a/public/viewjs/productgroupform.js b/public/viewjs/productgroupform.js index d4095fa4..86d5b33d 100644 --- a/public/viewjs/productgroupform.js +++ b/public/viewjs/productgroupform.js @@ -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) diff --git a/public/viewjs/productgroups.js b/public/viewjs/productgroups.js index d935977b..af64c63d 100644 --- a/public/viewjs/productgroups.js +++ b/public/viewjs/productgroups.js @@ -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(); + } +}); diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 2a040a7d..ace04d3d 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -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.') + '

' + __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.') + '

' + __t('Stock amount') + ': ' + stockAmount + ' ' + __n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), closeButton: false }); } diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 2f5ebad7..7fe1d843 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -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 })); + $('#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 })); diff --git a/public/viewjs/quantityunitconversionform.js b/public/viewjs/quantityunitconversionform.js index a21e74ed..5137fc7e 100644 --- a/public/viewjs/quantityunitconversionform.js +++ b/public/viewjs/quantityunitconversionform.js @@ -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"))); } }); }, diff --git a/public/viewjs/stockentries.js b/public/viewjs/stockentries.js index 8f173a05..20c9a8ec 100644 --- a/public/viewjs/stockentries.js +++ b/public/viewjs/stockentries.js @@ -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'); diff --git a/public/viewjs/stockentryform.js b/public/viewjs/stockentryform.js index 19817541..d0b41f81 100644 --- a/public/viewjs/stockentryform.js +++ b/public/viewjs/stockentryform.js @@ -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"); diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 4cb6e205..191ec9a7 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -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); diff --git a/public/viewjs/transfer.js b/public/viewjs/transfer.js index c803b303..bc3dc3bf 100644 --- a/public/viewjs/transfer.js +++ b/public/viewjs/transfer.js @@ -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); } ); } diff --git a/routes.php b/routes.php index c432ecee..4da065c0 100644 --- a/routes.php +++ b/routes.php @@ -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 diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 29d426a7..bea6c23f 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -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); diff --git a/services/StockService.php b/services/StockService.php index 273b790c..2abfae88 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -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 diff --git a/views/components/numberpicker.blade.php b/views/components/numberpicker.blade.php index 623e0103..3ffa3cb6 100644 --- a/views/components/numberpicker.blade.php +++ b/views/components/numberpicker.blade.php @@ -16,7 +16,7 @@ @php if(!isset($isRequired)) { $isRequired = true; } @endphp @php if(!isset($noNameAttribute)) { $noNameAttribute = false; } @endphp -
+
diff --git a/views/components/productcard.blade.php b/views/components/productcard.blade.php index b61a71d7..91b93aaa 100644 --- a/views/components/productcard.blade.php +++ b/views/components/productcard.blade.php @@ -21,12 +21,16 @@
- {{ $__t('Stock amount') . ' / ' . $__t('Quantity unit') }}: + {{ $__t('Stock amount') }}: + +
+ {{ $__t('Stock value') }}:
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING){{ $__t('Default location') }}:
@endif {{ $__t('Last purchased') }}:
{{ $__t('Last used') }}:
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING){{ $__t('Last price') }}:
@endif + @if (GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING){{ $__t('Average price') }}:
@endif @if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING){{ $__t('Average shelf life') }}:
@endif {{ $__t('Spoil rate') }}: diff --git a/views/components/productpicker.blade.php b/views/components/productpicker.blade.php index 99998514..2d2cf033 100644 --- a/views/components/productpicker.blade.php +++ b/views/components/productpicker.blade.php @@ -21,7 +21,7 @@
{{ $__t('You have to select a product') }}
diff --git a/views/consume.blade.php b/views/consume.blade.php index c5234717..a0471b05 100644 --- a/views/consume.blade.php +++ b/views/consume.blade.php @@ -30,6 +30,7 @@ @include('components.productpicker', array( 'products' => $products, + 'barcodes' => $barcodes, 'nextInputSelector' => '#amount', 'disallowAddProductWorkflows' => true )) diff --git a/views/inventory.blade.php b/views/inventory.blade.php index ea1f885e..95b8e717 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -13,6 +13,7 @@ @include('components.productpicker', array( 'products' => $products, + 'barcodes' => $barcodes, 'nextInputSelector' => '#new_amount' )) diff --git a/views/productbarcodesform.blade.php b/views/productbarcodesform.blade.php new file mode 100644 index 00000000..7f7b3228 --- /dev/null +++ b/views/productbarcodesform.blade.php @@ -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') +
+
+

@yield('title')

+
+
+
+
+
+ +

{{ $__t('Barcode for product') }} {{ $product->name }}

+ + + + @if($mode == 'edit') + + @endif + +
+ + + +
+ +
+ + @include('components.barcodescanner') +
+
+ + @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) +
+ + +
+ @else + + @endif + + + +
+
+
+@stop diff --git a/views/productform.blade.php b/views/productform.blade.php index 446ad7d7..5c566300 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -46,6 +46,14 @@
{{ $__t('A name is required') }}
+
+
+ + active == 1) checked @endif class="form-check-input" type="checkbox" id="active" name="active" value="1"> + +
+
+ @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 @@ -
- -
- -
-
-
- @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@@ -171,7 +170,8 @@
- @foreach($quantityunits as $quantityunit) @@ -295,7 +295,7 @@

{{ $__t('QU conversions') }} - + {{ $__t('Add') }}

@@ -316,7 +316,7 @@ @if($quConversion->product_id == $product->id || $quConversion->product_id == null) - + @@ -346,6 +346,55 @@ +

+ {{ $__t('Barcodes') }} + + {{ $__t('Add') }} + +

+
+ + + + + + + + + + + @if($mode == "edit") + @foreach($barcodes as $barcode) + @if($barcode->product_id == $product->id || $barcode->product_id == null) + + + + + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + + @endif + + @endif + @endforeach + @endif + +
{{ $__t('Barcode') }}{{ $__t('Factor purchase to stock quantity unit') }}{{ $__t('Store') }}
+ + + + + + + + {{ $barcode->barcode }} + + {{ $barcode->qu_factor_purchase_to_stock }} + + @if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id) !== null) + {{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id)->name }} + @endif +
+
diff --git a/views/productgroups.blade.php b/views/productgroups.blade.php index cb151700..96220ba1 100644 --- a/views/productgroups.blade.php +++ b/views/productgroups.blade.php @@ -20,7 +20,7 @@
@@ -55,7 +55,7 @@ @foreach($productGroups as $productGroup) - + diff --git a/views/products.blade.php b/views/products.blade.php index 42477567..94505c64 100644 --- a/views/products.blade.php +++ b/views/products.blade.php @@ -81,12 +81,12 @@ - + - {{ $product->name }}@if(!empty($product->picture_file_name)) @endif + @if($product->active == 0) (deactivated) @endif {{ $product->name }}@if(!empty($product->picture_file_name)) @endif {{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }} diff --git a/views/purchase.blade.php b/views/purchase.blade.php index d9068c07..10a579c7 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -30,6 +30,7 @@ @include('components.productpicker', array( 'products' => $products, + 'barcodes' => $barcodes, 'nextInputSelector' => '#amount' )) @@ -96,6 +97,16 @@ @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' => '

' + )) + @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @include('components.locationpicker', array( 'locations' => $locations, diff --git a/views/recipes.blade.php b/views/recipes.blade.php index b5b070e3..1408f87b 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -134,7 +134,7 @@
{{ $recipe->name }}

@if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1)@elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1)@else@endif - @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 + @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

@@ -250,7 +250,7 @@ @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
- +

{{ $costs }}

@endif @@ -310,6 +310,9 @@
{{ $selectedRecipePosition->product_group }}
@endif
  • + @if($selectedRecipePosition->product_active == 0) +
    {{ $__t('Deactivated Product') }}
    + @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)@elseif($selectedRecipePosition->need_fulfilled_with_shopping_list == 1)@else@endif - @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 + @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 @if(!empty($selectedRecipePosition->recipe_variable_amount))
    {{ $__t('Variable amount') }}
    diff --git a/views/stockentries.blade.php b/views/stockentries.blade.php index f31142d9..09724f21 100644 --- a/views/stockentries.blade.php +++ b/views/stockentries.blade.php @@ -41,6 +41,7 @@ {{ $__t('Store') }} {{ $__t('Price') }} @endif + {{ $__t('Factor purchase to stock') }} {{ $__t('Purchased date') }} @include('components.userfields_thead', array( @@ -158,6 +159,9 @@ {{ $stockEntry->price }} @endif + + {{ $stockEntry->qu_factor_purchase_to_stock }} + {{ $stockEntry->purchased_date }} diff --git a/views/stockentryform.blade.php b/views/stockentryform.blade.php index b9a6d2ee..39a55c1e 100644 --- a/views/stockentryform.blade.php +++ b/views/stockentryform.blade.php @@ -60,6 +60,16 @@ 'additionalHtmlContextHelp' => '
    ' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '
    ' )) + @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' => '

    ' + )) + @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @include('components.numberpicker', array( 'id' => 'price', diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index f479c670..9024be13 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -104,11 +104,11 @@ {{ $__t('Product') }} + {{ $__t('Product group') }} {{ $__t('Amount') }} {{ $__t('Next best before date') }} Hidden location Hidden status - Hidden product group @include('components.userfields_thead', array( 'userfields' => $userfields @@ -206,9 +206,16 @@ {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} + @php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp + + @if($productGroup !== null){{ $productGroup->name }}@endif + {{ $currentStockEntry->amount }} {{ $__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) }} @if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif + @if($currentStockEntry->amount != $currentStockEntry->factor_purchase_amount) + ({{ $currentStockEntry->factor_purchase_amount }} {{ $__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) }}) + @endif @if($currentStockEntry->is_aggregated_amount == 1) {{ $currentStockEntry->amount_aggregated }} {{ $__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 @@ @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 - @php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp - - @if($productGroup !== null){{ $productGroup->name }}@endif - @include('components.userfields_tbody', array( 'userfields' => $userfields, diff --git a/views/transfer.blade.php b/views/transfer.blade.php index f679dc7c..5f979481 100644 --- a/views/transfer.blade.php +++ b/views/transfer.blade.php @@ -14,6 +14,7 @@ @include('components.productpicker', array( 'products' => $products, + 'barcodes' => $barcodes, 'nextInputSelector' => '#location_id_from', 'disallowAddProductWorkflows' => true ))