Removed qu_factor_purchase_to_stock (migrated existing factors to normal product specific QU conversions)

This commit is contained in:
Bernd Bestel
2022-12-25 19:48:22 +01:00
parent dea6f3f820
commit 0229d187ae
12 changed files with 587 additions and 115 deletions

View File

@@ -1,6 +1,6 @@
> ⚠️ PHP 8.1 (with SQLite 3.34.0+) is from now on the only supported runtime version. > ⚠️ PHP 8.1 (with SQLite 3.34.0+) is from now on the only supported runtime version.
> xxxImportant upgrade informationXXX > The major version bump is due to breaking API changes, please see below if you use the API.
### New feature: xxxx ### New feature: xxxx
@@ -9,6 +9,9 @@
### Stock ### Stock
- Quantity unit conversions now support transitive conversions, means the QU hierarchy has now unlimited levels (thanks a lot @esclear) - Quantity unit conversions now support transitive conversions, means the QU hierarchy has now unlimited levels (thanks a lot @esclear)
- The product option "Factor purchase to stock quantity unit" was removed
- => Use normal product specific QU conversions instead, if needed
- An existing "Factor purchase to stock quantity unit" was automatically migrated to a product specific QU conversion
- Fixed that hiding the "Purchased date" column (table options) on the stock entries page didn't work - Fixed that hiding the "Purchased date" column (table options) on the stock entries page didn't work
- Fixed that the consumed amount was wrong, when consuming multiple substituted subproducts at once and when multiple/different conversion factors were involved - Fixed that the consumed amount was wrong, when consuming multiple substituted subproducts at once and when multiple/different conversion factors were involved
@@ -56,6 +59,9 @@
### API ### API
- ⚠️ **Breaking changes**:
- The product property `qu_factor_purchase_to_stock` was removed (existing factors were migrated to normal product specific QU conversions, see above)
- The endpoint `/stock/products/{productId}` returns a new field/property `qu_conversion_factor_purchase_to_stock` for convenience (contains the conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock)
- The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit) - The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit)
- `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units) - `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units)
- The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`) - The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`)

View File

@@ -163,6 +163,7 @@ class StockController extends BaseController
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunitsStock' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfields' => $this->getUserfieldsService()->GetFields('products'),
@@ -181,6 +182,7 @@ class StockController extends BaseController
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'), 'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'), 'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsPurchase' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), 'productgroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'), 'userfields' => $this->getUserfieldsService()->GetFields('products'),
@@ -503,7 +505,7 @@ class StockController extends BaseController
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'), 'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'), 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name', 'COLLATE NOCASE'),
'stockEntries' => $this->getDatabase()->stock()->orderBy('product_id'), 'stockEntries' => $this->getDatabase()->uihelper_stock_entries()->orderBy('product_id'),
'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(), 'currentStockLocations' => $this->getStockService()->GetCurrentStockLocations(),
'nextXDays' => $nextXDays, 'nextXDays' => $nextXDays,
'userfieldsProducts' => $this->getUserfieldsService()->GetFields('products'), 'userfieldsProducts' => $this->getUserfieldsService()->GetFields('products'),

View File

@@ -4437,9 +4437,6 @@
"product_group_id": { "product_group_id": {
"type": "integer" "type": "integer"
}, },
"qu_factor_purchase_to_stock": {
"type": "number"
},
"tare_weight": { "tare_weight": {
"type": "number" "type": "number"
}, },
@@ -4495,7 +4492,6 @@
"location_id": "4", "location_id": "4",
"qu_id_purchase": "3", "qu_id_purchase": "3",
"qu_id_stock": "3", "qu_id_stock": "3",
"qu_factor_purchase_to_stock": "1.0",
"min_stock_amount": "8", "min_stock_amount": "8",
"default_best_before_days": "0", "default_best_before_days": "0",
"row_created_timestamp": "2019-05-02 20:12:26", "row_created_timestamp": "2019-05-02 20:12:26",
@@ -4795,6 +4791,10 @@
}, },
"default_location": { "default_location": {
"$ref": "#/components/schemas/Location" "$ref": "#/components/schemas/Location"
},
"qu_conversion_factor_purchase_to_stock": {
"type": "number",
"description": "The conversion factor of the corresponding QU conversion from the product's qu_id_purchase to qu_id_stock"
} }
}, },
"example": { "example": {
@@ -4805,7 +4805,6 @@
"location_id": "4", "location_id": "4",
"qu_id_purchase": "3", "qu_id_purchase": "3",
"qu_id_stock": "3", "qu_id_stock": "3",
"qu_factor_purchase_to_stock": "1.0",
"min_stock_amount": "8", "min_stock_amount": "8",
"default_best_before_days": "0", "default_best_before_days": "0",
"row_created_timestamp": "2019-05-02 20:12:26", "row_created_timestamp": "2019-05-02 20:12:26",

View File

@@ -204,9 +204,6 @@ msgstr ""
msgid "Quantity unit stock" msgid "Quantity unit stock"
msgstr "" msgstr ""
msgid "Factor purchase to stock quantity unit"
msgstr ""
msgid "Create location" msgid "Create location"
msgstr "" msgstr ""
@@ -261,9 +258,6 @@ msgstr ""
msgid "Add products that are below defined min. stock amount" msgid "Add products that are below defined min. stock amount"
msgstr "" msgstr ""
msgid "This means 1 %1$s purchased will be converted into %2$s %3$s in stock"
msgstr ""
msgid "Login" msgid "Login"
msgstr "" msgstr ""

515
migrations/0207.sql Normal file
View File

@@ -0,0 +1,515 @@
-- Remove including the product's qu_factor_purchase_to_stock
DROP TRIGGER qu_conversions_custom_constraint_INS;
CREATE TRIGGER qu_conversions_custom_constraint_INS BEFORE INSERT ON quantity_unit_conversions
BEGIN
/*
Necessary because unique constraints don't include NULL values in SQLite
*/
SELECT CASE WHEN((
SELECT 1
FROM quantity_unit_conversions
WHERE from_qu_id = NEW.from_qu_id
AND to_qu_id = NEW.to_qu_id
AND IFNULL(product_id, 0) = IFNULL(NEW.product_id, 0)
)
NOTNULL) THEN RAISE(ABORT, "QU conversion already exists") END;
END;
-- Remove including the product's qu_factor_purchase_to_stock
DROP TRIGGER qu_conversions_custom_constraint_UPD;
CREATE TRIGGER qu_conversions_custom_constraint_UPD BEFORE UPDATE ON quantity_unit_conversions
BEGIN
/* This contains practically the same logic as the trigger qu_conversions_custom_constraint_INS */
/*
Necessary because unique constraints don't include NULL values in SQLite
*/
SELECT CASE WHEN((
SELECT 1
FROM quantity_unit_conversions
WHERE from_qu_id = NEW.from_qu_id
AND to_qu_id = NEW.to_qu_id
AND IFNULL(product_id, 0) = IFNULL(NEW.product_id, 0)
AND id != NEW.id
)
NOTNULL) THEN RAISE(ABORT, "QU conversion already exists") END;
END;
-- Migrate qu_factor_purchase_to_stock to product specific QU conversions
INSERT INTO quantity_unit_conversions
(from_qu_id, to_qu_id, factor, product_id)
SELECT p.qu_id_purchase, p.qu_id_stock, IFNULL(p.qu_factor_purchase_to_stock, 1.0), p.id
FROM products p
WHERE p.qu_id_stock != qu_id_purchase
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_purchase);
-- ALTER TABLE DROP COLUMN is only available in SQLite >= 3.35.0 (we require 3.34.0 as of now), so can't be used
PRAGMA legacy_alter_table = ON;
ALTER TABLE products RENAME TO products_old;
-- Remove qu_factor_purchase_to_stock column
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 CHECK(active IN (0, 1)),
location_id INTEGER NOT NULL,
shopping_location_id INTEGER,
qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER 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,
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,
due_type TINYINT NOT NULL DEFAULT 1 CHECK(due_type IN (1, 2)),
quick_consume_amount REAL NOT NULL DEFAULT 1,
hide_on_stock_overview TINYINT NOT NULL DEFAULT 0 CHECK(hide_on_stock_overview IN (0, 1)),
default_stock_label_type INTEGER NOT NULL DEFAULT 0,
should_not_be_frozen TINYINT NOT NULL DEFAULT 0 CHECK(should_not_be_frozen IN (0, 1)),
treat_opened_as_out_of_stock TINYINT NOT NULL DEFAULT 1 CHECK(treat_opened_as_out_of_stock IN (0, 1)),
no_own_stock TINYINT NOT NULL DEFAULT 0 CHECK(no_own_stock IN (0, 1)),
default_consume_location_id INTEGER,
move_on_open TINYINT NOT NULL DEFAULT 0 CHECK(move_on_open IN (0, 1)),
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO products
(id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing,
default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount,
hide_on_stock_overview, default_stock_label_type, should_not_be_frozen, row_created_timestamp,
treat_opened_as_out_of_stock, no_own_stock, default_consume_location_id, move_on_open)
SELECT id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing,
default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes, parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount,
hide_on_stock_overview, default_stock_label_type, should_not_be_frozen, row_created_timestamp,
treat_opened_as_out_of_stock, no_own_stock, default_consume_location_id, move_on_open
FROM products_old;
DROP TABLE products_old;
-- Recreate all products-table triggers and indexes
CREATE TRIGGER enforce_parent_product_id_null_when_empty_INS AFTER INSERT ON products
BEGIN
UPDATE products
SET parent_product_id = NULL
WHERE id = NEW.id
AND IFNULL(parent_product_id, '') = '';
END;
CREATE TRIGGER enforce_parent_product_id_null_when_empty_UPD AFTER UPDATE ON products
BEGIN
UPDATE products
SET parent_product_id = NULL
WHERE id = NEW.id
AND IFNULL(parent_product_id, '') = '';
END;
CREATE TRIGGER cascade_product_removal AFTER DELETE ON products
BEGIN
DELETE FROM stock
WHERE product_id = OLD.id;
DELETE FROM stock_log
WHERE product_id = OLD.id;
DELETE FROM product_barcodes
WHERE product_id = OLD.id;
DELETE FROM quantity_unit_conversions
WHERE product_id = OLD.id;
DELETE FROM recipes_pos
WHERE product_id = OLD.id;
UPDATE recipes
SET product_id = NULL
WHERE product_id = OLD.id;
DELETE FROM meal_plan
WHERE product_id = OLD.id
AND type = 'product';
DELETE FROM shopping_list
WHERE product_id = OLD.id;
DELETE FROM userfield_values
WHERE object_id = OLD.id
AND field_id IN (SELECT id FROM userfields WHERE entity = 'products');
END;
CREATE TRIGGER enfore_product_nesting_level BEFORE UPDATE ON products
BEGIN
-- Currently only 1 level is supported
SELECT CASE WHEN((
SELECT 1
FROM products p
WHERE IFNULL(NEW.parent_product_id, '') != ''
AND IFNULL(parent_product_id, '') = NEW.id
) NOTNULL) THEN RAISE(ABORT, "Unsupported product nesting level detected (currently only 1 level is supported)") END;
END;
CREATE TRIGGER enforce_min_stock_amount_for_cumulated_childs_INS AFTER INSERT ON products
BEGIN
/*
When a parent product has cumulate_min_stock_amount_of_sub_products enabled,
the child should not have any min_stock_amount
*/
UPDATE products
SET min_stock_amount = 0
WHERE id IN (
SELECT
p_child.id
FROM products p_parent
JOIN products p_child
ON p_child.parent_product_id = p_parent.id
WHERE p_parent.id = NEW.id
AND IFNULL(p_parent.cumulate_min_stock_amount_of_sub_products, 0) = 1
)
AND min_stock_amount > 0;
END;
CREATE TRIGGER enforce_min_stock_amount_for_cumulated_childs_UPD AFTER UPDATE ON products
BEGIN
/*
When a parent product has cumulate_min_stock_amount_of_sub_products enabled,
the child should not have any min_stock_amount
*/
UPDATE products
SET min_stock_amount = 0
WHERE id IN (
SELECT
p_child.id
FROM products p_parent
JOIN products p_child
ON p_child.parent_product_id = p_parent.id
WHERE p_parent.id = NEW.id
AND IFNULL(p_parent.cumulate_min_stock_amount_of_sub_products, 0) = 1
)
AND min_stock_amount > 0;
END;
CREATE TRIGGER cascade_change_qu_id_stock BEFORE UPDATE ON products WHEN NEW.qu_id_stock != OLD.qu_id_stock
BEGIN
-- All amounts anywhere are related to the products stock QU,
-- so apply the appropriate unit conversion to all amounts everywhere on change
-- (and enforce that such a conversion need to exist when the product was once added to stock)
SELECT CASE WHEN((
SELECT 1
FROM quantity_unit_conversions_resolved
WHERE product_id = NEW.id
AND from_qu_id = OLD.qu_id_stock
AND to_qu_id = NEW.qu_id_stock
) ISNULL)
AND
((
SELECT 1
FROM stock_log
WHERE product_id = NEW.id
AND NEW.qu_id_stock != OLD.qu_id_stock
) NOTNULL) THEN RAISE(ABORT, "qu_id_stock can only be changed when a corresponding QU conversion (old QU => new QU) exists when the product was once added to stock") END;
UPDATE chores
SET product_amount = product_amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE product_id = NEW.id;
UPDATE meal_plan
SET product_amount = product_amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE type = 'product'
AND product_id = NEW.id;
UPDATE recipes_pos
SET amount = amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE product_id = NEW.id;
UPDATE shopping_list
SET amount = amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE product_id = NEW.id
AND product_id IS NOT NULL;
UPDATE stock
SET amount = amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0),
price = price / IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE product_id = NEW.id;
UPDATE stock_log
SET amount = amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0),
price = price / IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE product_id = NEW.id;
END;
CREATE TRIGGER cascade_change_qu_id_stock2 AFTER UPDATE ON products WHEN NEW.qu_id_stock != OLD.qu_id_stock
BEGIN
-- See also the trigger "cascade_change_qu_id_stock BEFORE UPDATE ON products"
-- This here applies the needed changes to the products table itself only AFTER the udpate
UPDATE products
SET quick_consume_amount = quick_consume_amount * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0),
calories = calories / IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0),
tare_weight = tare_weight * IFNULL((SELECT factor FROM quantity_unit_conversions_resolved WHERE product_id = NEW.id AND from_qu_id = OLD.qu_id_stock AND to_qu_id = NEW.qu_id_stock LIMIT 1), 1.0)
WHERE id = NEW.id;
END;
CREATE INDEX ix_products_performance1 ON products (
parent_product_id
);
CREATE INDEX ix_products_performance2 ON products (
CASE WHEN parent_product_id IS NULL THEN id ELSE parent_product_id END,
active
);
-- Remove including the product's qu_factor_purchase_to_stock
DROP VIEW quantity_unit_conversions_resolved;
CREATE VIEW quantity_unit_conversions_resolved
AS
/*
First, determine conversions that are a single step.
There may be multiple definitions for conversions between two units
(e.g. due to purchase-to-stock, product-specific and default conversions),
thus priorities are used to disambiguate conversions.
Later, we'll only use the factor with the highest priority to convert between two units.
*/
WITH RECURSIVE conversion_factors_dup(product_id, from_qu_id, to_qu_id, factor, priority)
AS (
-- Priority 1: Product specific QU overrides
-- Note that the quantity_unit_conversions table already contains both conversion directions for every conversion.
SELECT
product_id,
from_qu_id,
to_qu_id,
factor,
30
FROM quantity_unit_conversions
WHERE product_id IS NOT NULL
UNION
-- Priority 2: Default QU conversions are handled in a later CTE, as we can't determine yet, for which products they are applicable.
SELECT
product_id,
from_qu_id,
to_qu_id,
factor,
20
FROM quantity_unit_conversions
WHERE product_id IS NULL
UNION
-- Priority 3: QU conversions with a factor of 1.0 from the stock unit to the stock unit
SELECT
id,
qu_id_stock,
qu_id_stock,
1.0,
10
FROM products
),
-- Now, remove duplicate conversions, only retaining the entries with the highest priority
conversion_factors(product_id, from_qu_id, to_qu_id, factor)
AS (
SELECT
product_id,
from_qu_id,
to_qu_id,
FIRST_VALUE(factor) OVER win
FROM conversion_factors_dup
GROUP BY product_id, from_qu_id, to_qu_id
WINDOW win AS(PARTITION BY product_id, from_qu_id, to_qu_id ORDER BY priority DESC)
),
-- Now build the closure of posisble conversions using a recursive CTE
closure(depth, product_id, from_qu_id, to_qu_id, factor, path)
AS (
-- As a base case, select the conversions that refer to a concrete product
SELECT
1 as depth,
product_id,
from_qu_id,
to_qu_id,
factor,
'/' || from_qu_id || '/' || to_qu_id || '/' -- We need to keep track of the conversion path in order to prevent cycles
FROM conversion_factors
WHERE product_id IS NOT NULL
UNION
-- First recursive case: Add a product-associated conversion to the chain
SELECT
c.depth + 1,
c.product_id,
c.from_qu_id,
s.to_qu_id,
c.factor * s.factor,
c.path || s.to_qu_id || '/'
FROM closure c
JOIN conversion_factors s
ON c.product_id = s.product_id
AND c.to_qu_id = s.from_qu_id
WHERE c.path NOT LIKE ('%/' || s.to_qu_id || '/%') -- Prevent cycles
UNION
-- Second recursive case: Add a default unit conversion to the *start* of the conversion chain
SELECT
c.depth + 1,
c.product_id,
s.from_qu_id,
c.to_qu_id,
s.factor * c.factor,
'/' || s.from_qu_id || c.path
FROM closure c
JOIN conversion_factors s
ON s.to_qu_id = c.from_qu_id
AND s.product_id IS NULL
WHERE NOT EXISTS(SELECT 1 FROM conversion_factors ci WHERE ci.product_id = c.product_id AND ci.from_qu_id = s.from_qu_id AND ci.to_qu_id = s.to_qu_id) -- Do this only, if there is no product_specific conversion between the units in s
AND c.path NOT LIKE ('%/' || s.from_qu_id || '/%') -- Prevent cycles
UNION
-- Third recursive case: Add a default unit conversion to the *end* of the conversion chain
SELECT
c.depth + 1,
c.product_id,
c.from_qu_id,
s.to_qu_id,
c.factor * s.factor,
c.path || s.to_qu_id || '/'
FROM closure c
JOIN conversion_factors s
ON c.to_qu_id = s.from_qu_id
AND s.product_id IS NULL
WHERE NOT EXISTS(SELECT 1 FROM conversion_factors ci WHERE ci.product_id = c.product_id AND ci.from_qu_id = s.from_qu_id AND ci.to_qu_id = s.to_qu_id) -- Do this only, if there is no product_specific conversion between the units in s
AND c.path NOT LIKE ('%/' || s.to_qu_id || '/%') -- Prevent cycles
UNION
-- Fourth case: Add the default unit conversions that are reachable by a given product.
-- We cannot start with them directly, as we only want to add default conversions,
-- where at least one of the units is 'reachable' from the product's stock quantity unit.
-- Thus we add these cases here.
SELECT DISTINCT
1, c.product_id,
s.from_qu_id, s.to_qu_id,
s.factor,
'/' || s.from_qu_id || '/' || s.to_qu_id || '/'
FROM closure c, conversion_factors s
WHERE NOT EXISTS(SELECT 1 FROM conversion_factors ci WHERE ci.product_id = c.product_id AND ci.from_qu_id = s.from_qu_id AND ci.to_qu_id = s.to_qu_id)
AND c.path LIKE ('%/' || s.from_qu_id || '/' || s.to_qu_id || '/%') -- Prevent cycles
)
SELECT DISTINCT
-1 AS id, -- Dummy, LessQL needs an id column
c.product_id,
c.from_qu_id,
qu_from.name AS from_qu_name,
qu_from.name_plural AS from_qu_name_plural,
c.to_qu_id,
qu_to.name AS to_qu_name,
qu_to.name_plural AS to_qu_name_plural,
FIRST_VALUE(factor) OVER win AS factor,
FIRST_VALUE(c.path) OVER win AS path
FROM closure c
JOIN quantity_units qu_from
ON c.from_qu_id = qu_from.id
JOIN quantity_units qu_to
ON c.to_qu_id = qu_to.id
GROUP BY product_id, from_qu_id, to_qu_id
WINDOW win AS (PARTITION BY product_id, from_qu_id, to_qu_id ORDER BY depth ASC)
ORDER BY product_id, from_qu_id, to_qu_id;
DROP VIEW uihelper_stock_current_overview;
CREATE VIEW uihelper_stock_current_overview
AS
SELECT
p.id,
sc.amount_opened AS amount_opened,
p.tare_weight AS tare_weight,
p.enable_tare_weight_handling AS enable_tare_weight_handling,
sc.amount AS amount,
sc.value as value,
sc.product_id AS product_id,
sc.best_before_date AS best_before_date,
EXISTS(SELECT id FROM stock_missing_products WHERE id = sc.product_id) AS product_missing,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_stock) AS qu_unit_name_plural,
p.name AS product_name,
(SELECT name FROM product_groups WHERE product_groups.id = p.product_group_id) AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
(SELECT name FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name,
(SELECT name_plural FROM quantity_units WHERE quantity_units.id = p.qu_id_purchase) AS qu_purchase_unit_name_plural,
sc.is_aggregated_amount,
sc.amount_opened_aggregated,
sc.amount_aggregated,
p.calories AS product_calories,
sc.amount * p.calories AS calories,
sc.amount_aggregated * p.calories AS calories_aggregated,
p.quick_consume_amount,
p.due_type,
plp.purchased_date AS last_purchased,
plp.price AS last_price,
pap.price as average_price,
p.min_stock_amount,
pbcs.barcodes AS product_barcodes,
p.description AS product_description,
l.name AS product_default_location_name,
p_parent.id AS parent_product_id,
p_parent.name AS parent_product_name,
p.picture_file_name AS product_picture_file_name,
p.no_own_stock AS product_no_own_stock,
IFNULL(quc.factor, 1.0) AS product_qu_factor_purchase_to_stock
FROM (
SELECT *
FROM stock_current
WHERE best_before_date IS NOT NULL
UNION
SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type
FROM stock_missing_products m
JOIN products p
ON m.id = p.id
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc
LEFT JOIN products_last_purchased plp
ON sc.product_id = plp.product_id
LEFT JOIN products_average_price pap
ON sc.product_id = pap.product_id
LEFT JOIN products p
ON sc.product_id = p.id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sc.product_id = pbcs.product_id
LEFT JOIN products p_parent
ON p.parent_product_id = p_parent.id
LEFT JOIN locations l
ON p.location_id = l.id
LEFT JOIN quantity_unit_conversions quc
ON sc.product_id = quc.product_id
AND p.qu_id_purchase = quc.from_qu_id
AND p.qu_id_stock = quc.to_qu_id
WHERE p.hide_on_stock_overview = 0;
CREATE VIEW uihelper_stock_entries
AS
SELECT
*,
IFNULL(quc.factor, 1.0) AS product_qu_factor_purchase_to_stock
FROM stock s
JOIN products p
ON s.product_id = p.id
LEFT JOIN quantity_unit_conversions quc
ON s.product_id = quc.product_id
AND p.qu_id_purchase = quc.from_qu_id
AND p.qu_id_stock = quc.to_qu_id;

View File

@@ -84,7 +84,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null) if (productDetails.last_price !== null)
{ {
$('#productcard-product-last-price').text(__t("%1$s per %2$s", (Number.parseFloat(productDetails.last_price) * Number.parseFloat(productDetails.product.qu_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name)); $('#productcard-product-last-price').text(__t("%1$s per %2$s", (Number.parseFloat(productDetails.last_price) * Number.parseFloat(productDetails.qu_conversion_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name));
$('#productcard-product-last-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(productDetails.last_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name)); $('#productcard-product-last-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(productDetails.last_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name));
} }
else else
@@ -95,7 +95,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.avg_price !== null) if (productDetails.avg_price !== null)
{ {
$('#productcard-product-average-price').text(__t("%1$s per %2$s", (Number.parseFloat(productDetails.avg_price) * Number.parseFloat(productDetails.product.qu_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name)); $('#productcard-product-average-price').text(__t("%1$s per %2$s", (Number.parseFloat(productDetails.avg_price) * Number.parseFloat(productDetails.qu_conversion_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name));
$('#productcard-product-average-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(productDetails.avg_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name)); $('#productcard-product-average-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(productDetails.avg_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name));
} }
else else
@@ -151,7 +151,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
datasets[key] = [] datasets[key] = []
} }
chart.labels.push(moment(dataPoint.date).toDate()); chart.labels.push(moment(dataPoint.date).toDate());
datasets[key].push({ x: moment(dataPoint.date).toDate(), y: Number.parseFloat(dataPoint.price) * Number.parseFloat(productDetails.product.qu_factor_purchase_to_stock) }); datasets[key].push({ x: moment(dataPoint.date).toDate(), y: Number.parseFloat(dataPoint.price) * Number.parseFloat(productDetails.qu_conversion_factor_purchase_to_stock) });
}); });
Object.keys(datasets).forEach((key) => Object.keys(datasets).forEach((key) =>

View File

@@ -91,7 +91,6 @@ $('.save-product-button').on('click', function(e)
var parentProductId = jsonData.product_id; var parentProductId = jsonData.product_id;
delete jsonData.product_id; delete jsonData.product_id;
jsonData.parent_product_id = parentProductId; jsonData.parent_product_id = parentProductId;
jsonData.qu_factor_purchase_to_stock = $("#qu_factor_purchase_to_stock").val(); // Set manually due to input could be disabled
Grocy.FrontendHelpers.BeginUiBusy("product-form"); Grocy.FrontendHelpers.BeginUiBusy("product-form");
if (jsonData.parent_product_id.toString().isEmpty()) if (jsonData.parent_product_id.toString().isEmpty())
@@ -160,32 +159,6 @@ if (GetUriParam("flow") !== undefined || GetUriParam("returnto") !== undefined)
$('.input-group-qu').on('change', function(e) $('.input-group-qu').on('change', function(e)
{ {
var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val();
if (Grocy.EditMode == "create" && !quIdPurchase.toString().isEmpty() && !quIdStock.toString().isEmpty() && quIdPurchase != quIdStock)
{
Grocy.Api.Get("objects/quantity_unit_conversions?query[]=product_id=null&query[]=from_qu_id=" + quIdPurchase + "&query[]=to_qu_id=" + quIdStock,
function(response)
{
if (response != null && response.length > 0)
{
var conversion = response[0];
$("#qu_factor_purchase_to_stock").val(conversion.factor);
RefreshLocaleNumberInput("#qu_factor_purchase_to_stock");
RefreshQuConversionInfo();
}
},
function(xhr)
{
console.error(xhr);
}
);
}
RefreshQuConversionInfo();
$("#tare_weight_qu_info").text($("#qu_id_stock option:selected").text()); $("#tare_weight_qu_info").text($("#qu_id_stock option:selected").text());
$("#quick_consume_qu_info").text($("#qu_id_stock option:selected").text()); $("#quick_consume_qu_info").text($("#qu_id_stock option:selected").text());
$("#energy_qu_info").text($("#qu_id_stock option:selected").text()); $("#energy_qu_info").text($("#qu_id_stock option:selected").text());
@@ -193,32 +166,6 @@ $('.input-group-qu').on('change', function(e)
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
}); });
function RefreshQuConversionInfo()
{
var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val();
var factor = $('#qu_factor_purchase_to_stock').val();
if (factor > 1 && quIdPurchase != quIdStock)
{
$('#qu-conversion-info').text(__t('This means 1 %1$s purchased will be converted into %2$s %3$s in stock', $("#qu_id_purchase option:selected").text(), (1 * factor).toString(), __n((1 * factor).toString(), $("#qu_id_stock option:selected").text(), $("#qu_id_stock option:selected").data("plural-form"), true)));
$('#qu-conversion-info').removeClass('d-none');
}
else
{
$('#qu-conversion-info').addClass('d-none');
}
if (quIdStock == quIdPurchase)
{
$("#qu_factor_purchase_to_stock").attr("disabled", "");
}
else
{
$("#qu_factor_purchase_to_stock").removeAttr("disabled");
}
}
$('#product-form input').keyup(function(event) $('#product-form input').keyup(function(event)
{ {
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
@@ -420,8 +367,6 @@ $('#qu_id_stock').change(function(e)
quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex; quIdPurchase[0].selectedIndex = quIdStock[0].selectedIndex;
Grocy.FrontendHelpers.ValidateForm('product-form'); Grocy.FrontendHelpers.ValidateForm('product-form');
} }
RefreshQuConversionInfo();
}); });
$(window).on("message", function(e) $(window).on("message", function(e)
@@ -465,7 +410,6 @@ if (Grocy.EditMode == "create" && GetUriParam("copy-of") != undefined)
} }
$("#qu_id_stock").val(sourceProduct.qu_id_stock); $("#qu_id_stock").val(sourceProduct.qu_id_stock);
$("#qu_id_purchase").val(sourceProduct.qu_id_purchase); $("#qu_id_purchase").val(sourceProduct.qu_id_purchase);
$("#qu_factor_purchase_to_stock").val(sourceProduct.qu_factor_purchase_to_stock);
if (BoolVal(sourceProduct.enable_tare_weight_handling)) if (BoolVal(sourceProduct.enable_tare_weight_handling))
{ {
$("#enable_tare_weight_handling").prop("checked", true); $("#enable_tare_weight_handling").prop("checked", true);

View File

@@ -254,7 +254,7 @@ function RefreshStockEntryRow(stockRowId)
result.price = 0; result.price = 0;
} }
$('#stock-' + stockRowId + '-price').text(__t("%1$s per %2$s", (Number.parseFloat(result.price) * Number.parseFloat(productDetails.product.qu_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name)); $('#stock-' + stockRowId + '-price').text(__t("%1$s per %2$s", (Number.parseFloat(result.price) * Number.parseFloat(productDetails.qu_conversion_factor_purchase_to_stock)).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.default_quantity_unit_purchase.name));
$('#stock-' + stockRowId + '-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(result.price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name)); $('#stock-' + stockRowId + '-price').attr("data-original-title", __t("%1$s per %2$s", Number.parseFloat(result.price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices_display }), productDetails.quantity_unit_stock.name));
}, },
function(xhr) function(xhr)

View File

@@ -76,32 +76,32 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO product_groups(name) VALUES ('06 {$this->__t_sql('Refrigerated products')}'); --6 INSERT INTO product_groups(name) VALUES ('06 {$this->__t_sql('Refrigerated products')}'); --6
DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here... DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here...
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Cookies')}', 4, 3, 3, 1, 8, 1, 'cookies.jpg'); --1 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Cookies')}', 4, 3, 3, 8, 1, 'cookies.jpg'); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, cumulate_min_stock_amount_of_sub_products) VALUES ('{$this->__t_sql('Chocolate')}', 4, 3, 3, 1, 8, 1, 1); --2 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, cumulate_min_stock_amount_of_sub_products) VALUES ('{$this->__t_sql('Chocolate')}', 4, 3, 3, 8, 1, 1); --2
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Gummy bears')}', 4, 3, 3, 1, 8, 1, 'gummybears.jpg'); --3 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Gummy bears')}', 4, 3, 3, 8, 1, 'gummybears.jpg'); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$this->__t_sql('Crisps')}', 4, 3, 3, 1, 10, 1); --4 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id) VALUES ('{$this->__t_sql('Crisps')}', 4, 3, 3, 10, 1); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Eggs')}', 2, 3, 2, 10, 5); --5 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Eggs')}', 2, 3, 2, 5); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Noodles')}', 3, 3, 3, 1, 6); --6 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Noodles')}', 3, 3, 3, 6); --6
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Pickles')}', 5, 4, 4, 1, 3); --7 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Pickles')}', 5, 4, 4, 3); --7
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Gulash soup')}', 5, 5, 5, 1, 3); --8 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Gulash soup')}', 5, 5, 5, 3); --8
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Yogurt')}', 2, 6, 6, 1, 6); --9 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Yogurt')}', 2, 6, 6, 6); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Cheese')}', 2, 3, 3, 1, 6); --10 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Cheese')}', 2, 3, 3, 6); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, description) VALUES ('{$this->__t_sql('Cold cuts')}', 2, 3, 3, 1, 6, '{$loremIpsum}'); --11 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, description) VALUES ('{$this->__t_sql('Cold cuts')}', 2, 3, 3, 6, '{$loremIpsum}'); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Paprika')}', 2, 2, 2, 1, 5, 'paprika.jpg', 7); --12 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Paprika')}', 2, 2, 2, 5, 'paprika.jpg', 7); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Cucumber')}', 2, 2, 2, 1, 5, 'cucumber.jpg', 7); --13 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Cucumber')}', 2, 2, 2, 5, 'cucumber.jpg', 7); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, default_best_before_days) VALUES ('{$this->__t_sql('Radish')}', 2, 7, 7, 1, 5, 7); --14 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, default_best_before_days) VALUES ('{$this->__t_sql('Radish')}', 2, 7, 7, 5, 7); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Tomato')}', 2, 2, 2, 1, 5, 'tomato.jpg', 7); --15 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$this->__t_sql('Tomato')}', 2, 2, 2, 5, 'tomato.jpg', 7); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Pizza dough')}', 2, 3, 3, 1, 6); --16 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Pizza dough')}', 2, 3, 3, 6); --16
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Sieved tomatoes')}', 5, 5, 5, 1, 3); --17 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Sieved tomatoes')}', 5, 5, 5, 3); --17
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Salami')}', 2, 3, 3, 1, 6); --18 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Salami')}', 2, 3, 3, 6); --18
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Toast')}', 3, 5, 5, 1, 2); --19 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Toast')}', 3, 5, 5, 2); --19
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, default_best_before_days_after_freezing, default_best_before_days_after_thawing, due_type) VALUES ('{$this->__t_sql('Minced meat')}', 2, 3, 3, 1, 4, 180, 2, 2); --20 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, default_best_before_days_after_freezing, default_best_before_days_after_thawing, due_type) VALUES ('{$this->__t_sql('Minced meat')}', 2, 3, 3, 4, 180, 2, 2); --20
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, enable_tare_weight_handling, tare_weight, calories) VALUES ('{$this->__t_sql('Flour')}', 3, 8, 8, 1, 3, 1, 500, 2); --21 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, enable_tare_weight_handling, tare_weight, calories) VALUES ('{$this->__t_sql('Flour')}', 3, 8, 8, 3, 1, 500, 2); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Sugar')}', 3, 3, 3, 1, 3, 3870); --22 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Sugar')}', 3, 3, 3, 3, 3870); --22
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Milk')}', 2, 10, 10, 1, 6, 418); --23 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, calories) VALUES ('{$this->__t_sql('Milk')}', 2, 10, 10, 6, 418); --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, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Milk Chocolate')}', 4, 3, 3, 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, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Dark Chocolate')}', 4, 3, 3, 1, 2); --25
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 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Waffle rolls')}', 4, 3, 3, 1); --26
UPDATE products SET calories = 123 WHERE IFNULL(calories, 0) = 0; UPDATE products SET calories = 123 WHERE IFNULL(calories, 0) = 0;
INSERT INTO product_barcodes (product_id, barcode) VALUES (8, '22111968'); INSERT INTO product_barcodes (product_id, barcode) VALUES (8, '22111968');
@@ -119,6 +119,7 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (3, 8, 200, 18); INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (3, 8, 200, 18);
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor) VALUES (13, 8, 1000); INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor) VALUES (13, 8, 1000);
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor) VALUES (9, 11, 1000); INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor) VALUES (9, 11, 1000);
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor, product_id) VALUES (3, 2, 10, 5);
INSERT INTO shopping_list (note, amount) VALUES ('{$this->__t_sql('Some good snacks')}', 1); INSERT INTO shopping_list (note, amount) VALUES ('{$this->__t_sql('Some good snacks')}', 1);
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1); INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);

View File

@@ -600,14 +600,26 @@ class StockService extends BaseService
{ {
// Add product to database and include new product id in output // Add product to database and include new product id in output
$productData = $pluginOutput; $productData = $pluginOutput;
unset($productData['barcode']); unset($productData['barcode'], $productData['qu_factor_purchase_to_stock']);
$newProductRow = $this->getDatabase()->products()->createRow($productData); $newProductRow = $this->getDatabase()->products()->createRow($productData);
$newProductRow->save(); $newProductRow->save();
$this->getDatabase()->product_barcodes()->createRow([ $this->getDatabase()->product_barcodes()->createRow([
'product_id' => $newProductRow->id, 'product_id' => $newProductRow->id,
'barcode' => $pluginOutput['barcode'] 'barcode' => $pluginOutput['barcode']
])->save(); ])->save();
if ($pluginOutput['qu_id_stock'] != $pluginOutput['qu_id_purchase'])
{
$this->getDatabase()->quantity_unit_conversions()->createRow([
'product_id' => $newProductRow->id,
'from_qu_id' => $pluginOutput['qu_id_purchase'],
'to_qu_id' => $pluginOutput['qu_id_stock'],
'factor' => $pluginOutput['qu_factor_purchase_to_stock'],
])->save();
}
$pluginOutput['id'] = $newProductRow->id; $pluginOutput['id'] = $newProductRow->id;
} }
} }
@@ -753,6 +765,16 @@ class StockService extends BaseService
$defaultConsumeLocation = $this->getDatabase()->locations($product->default_consume_location_id); $defaultConsumeLocation = $this->getDatabase()->locations($product->default_consume_location_id);
} }
$quConversionFactorPurchaseToStock = 1.0;
if ($product->qu_id_stock != $product->qu_id_purchase)
{
$conversion = $this->getDatabase()->quantity_unit_conversions()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_purchase, $product->qu_id_stock)->fetch();
if ($conversion != null)
{
$quConversionFactorPurchaseToStock = $conversion->factor;
}
}
return [ return [
'product' => $product, 'product' => $product,
'product_barcodes' => $productBarcodes, 'product_barcodes' => $productBarcodes,
@@ -777,7 +799,8 @@ class StockService extends BaseService
'spoil_rate_percent' => $spoilRate, 'spoil_rate_percent' => $spoilRate,
'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount, 'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount,
'has_childs' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0, 'has_childs' => $this->getDatabase()->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
'default_consume_location' => $defaultConsumeLocation 'default_consume_location' => $defaultConsumeLocation,
'qu_conversion_factor_purchase_to_stock' => $quConversionFactorPurchaseToStock
]; ];
} }

View File

@@ -395,7 +395,7 @@
id="qu_id_purchase" id="qu_id_purchase"
name="qu_id_purchase"> name="qu_id_purchase">
<option></option> <option></option>
@foreach($quantityunits as $quantityunit) @foreach($quantityunitsPurchase as $quantityunit)
<option @if($mode=='edit' <option @if($mode=='edit'
&& &&
$quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option> $quantityunit->id == $product->qu_id_purchase) selected="selected" @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option>
@@ -404,18 +404,6 @@
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div> <div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
</div> </div>
@php if($mode == 'edit') { $value = $product->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' => $DEFAULT_MIN_AMOUNT,
'decimals' => $userSettings['stock_decimal_places_amounts'],
'value' => $value,
'additionalCssClasses' => 'input-group-qu locale-number-input locale-number-quantity-amount',
'additionalHtmlElements' => '<p id="qu-conversion-info"
class="form-text text-info d-none"></p>'
))
<div class="form-group mb-1"> <div class="form-group mb-1">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input @if($mode=='edit' <input @if($mode=='edit'

View File

@@ -287,7 +287,7 @@
data-trigger="hover click" data-trigger="hover click"
data-html="true" data-html="true"
title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $stockEntry->price . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name) !!}"> title="{!! $__t('%1$s per %2$s', '<span class=\'locale-number locale-number-currency\'>' . $stockEntry->price . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_stock)->name) !!}">
{!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($stockEntry->price) * floatval(FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_factor_purchase_to_stock) . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_purchase)->name) !!} {!! $__t('%1$s per %2$s', '<span class="locale-number locale-number-currency">' . floatval($stockEntry->price) * floatval($stockEntry->product_qu_factor_purchase_to_stock) . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $stockEntry->product_id)->qu_id_purchase)->name) !!}
</span> </span>
</td> </td>
<td> <td>