Fix is_aggregated_amount of stock_current did not work anymore

This commit is contained in:
Bernd Bestel
2020-11-10 20:11:43 +01:00
parent 62e8d88adb
commit 8c54131921
13 changed files with 532 additions and 578 deletions

View File

@@ -141,20 +141,3 @@ WHERE pr.parent_product_id != pr.sub_product_id
GROUP BY pr.sub_product_id GROUP BY pr.sub_product_id
HAVING SUM(s.amount) > 0; 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;

View File

@@ -1,29 +1,61 @@
CREATE INDEX ix_products_performance1 ON products ( DROP VIEW stock_missing_products_including_opened;
parent_product_id CREATE VIEW stock_missing_products_including_opened
);
CREATE INDEX ix_products_performance2 ON products (
CASE WHEN parent_product_id IS NULL THEN id ELSE parent_product_id END,
active
);
CREATE INDEX ix_stock_performance1 ON stock (
product_id,
open,
best_before_date,
amount
);
DROP VIEW products_resolved;
CREATE VIEW products_resolved
AS AS
/* This is basically the same view as stock_missing_products, but the column "amount_missing" includes opened amounts */
-- Products WITHOUT sub products where the amount of the sub products SHOULD NOT be cumulated
SELECT SELECT
CASE p.id,
WHEN p.parent_product_id IS NULL THEN MAX(p.name) AS name,
p.id p.min_stock_amount - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing,
ELSE CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
p.parent_product_id FROM products_view p
END AS parent_product_id, LEFT JOIN stock_current s
p.id as sub_product_id ON p.id = s.product_id
WHERE p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
AND p.has_sub_products = 0
AND p.parent_product_id IS NULL
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0) < p.min_stock_amount
UNION
-- Parent products WITH sub products where the amount of the sub products SHOULD be cumulated
SELECT
p.id,
MAX(p.name) AS name,
SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount_aggregated), 0) - IFNULL(SUM(s.amount_opened_aggregated), 0)) AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products_view p
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 1
GROUP BY p.id
HAVING IFNULL(SUM(s.amount_aggregated), 0) - IFNULL(SUM(s.amount_opened_aggregated), 0) < SUM(sub_p.min_stock_amount)
UNION
-- Sub products where the amount SHOULD NOT be cumulated into the parent product
SELECT
sub_p.id,
MAX(sub_p.name) AS name,
SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products p FROM products p
WHERE p.active = 1; JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
GROUP BY sub_p.id
HAVING IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0) < sub_p.min_stock_amount;

View File

@@ -1,61 +1,27 @@
DROP VIEW stock_missing_products_including_opened; DELETE FROM shopping_list
CREATE VIEW stock_missing_products_including_opened WHERE shopping_list_id NOT IN (SELECT id FROM shopping_lists);
AS
/* This is basically the same view as stock_missing_products, but the column "amount_missing" includes opened amounts */ CREATE TRIGGER remove_items_from_deleted_shopping_list AFTER DELETE ON shopping_lists
BEGIN
DELETE FROM shopping_list WHERE shopping_list_id = OLD.id;
END;
-- Products WITHOUT sub products where the amount of the sub products SHOULD NOT be cumulated CREATE TRIGGER prevent_infinite_nested_recipes_INS BEFORE INSERT ON recipes_nestings
SELECT BEGIN
p.id, SELECT CASE WHEN((
MAX(p.name) AS name, SELECT 1
p.min_stock_amount - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing, FROM recipes_nestings_resolved rnr
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock WHERE NEW.recipe_id = rnr.includes_recipe_id
FROM products_view p AND NEW.includes_recipe_id = rnr.recipe_id
LEFT JOIN stock_current s ) NOTNULL) THEN RAISE(ABORT, "Recursive nested recipe detected") END;
ON p.id = s.product_id END;
WHERE p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
AND p.has_sub_products = 0
AND p.parent_product_id IS NULL
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0) < p.min_stock_amount
UNION CREATE TRIGGER prevent_infinite_nested_recipes_UPD BEFORE UPDATE ON recipes_nestings
BEGIN
-- Parent products WITH sub products where the amount of the sub products SHOULD be cumulated SELECT CASE WHEN((
SELECT SELECT 1
p.id, FROM recipes_nestings_resolved rnr
MAX(p.name) AS name, WHERE NEW.recipe_id = rnr.includes_recipe_id
SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount_aggregated), 0) - IFNULL(SUM(s.amount_opened_aggregated), 0)) AS amount_missing, AND NEW.includes_recipe_id = rnr.recipe_id
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock ) NOTNULL) THEN RAISE(ABORT, "Recursive nested recipe detected") END;
FROM products_view p END;
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 1
GROUP BY p.id
HAVING IFNULL(SUM(s.amount_aggregated), 0) - IFNULL(SUM(s.amount_opened_aggregated), 0) < SUM(sub_p.min_stock_amount)
UNION
-- Sub products where the amount SHOULD NOT be cumulated into the parent product
SELECT
sub_p.id,
MAX(sub_p.name) AS name,
SUM(sub_p.min_stock_amount) - (IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0)) AS amount_missing,
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products p
JOIN products_resolved pr
ON p.id = pr.parent_product_id
JOIN products sub_p
ON pr.sub_product_id = sub_p.id
LEFT JOIN stock_current s
ON pr.sub_product_id = s.product_id
WHERE sub_p.min_stock_amount != 0
AND p.cumulate_min_stock_amount_of_sub_products = 0
GROUP BY sub_p.id
HAVING IFNULL(SUM(s.amount), 0) - IFNULL(SUM(s.amount_opened), 0) < sub_p.min_stock_amount;

View File

@@ -1,27 +1,46 @@
DELETE FROM shopping_list DROP VIEW stock_current;
WHERE shopping_list_id NOT IN (SELECT id FROM shopping_lists); 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,
SUM(s.amount * IFNULL(qucr.factor, 1.0)) 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
CREATE TRIGGER remove_items_from_deleted_shopping_list AFTER DELETE ON shopping_lists UNION
BEGIN
DELETE FROM shopping_list WHERE shopping_list_id = OLD.id;
END;
CREATE TRIGGER prevent_infinite_nested_recipes_INS BEFORE INSERT ON recipes_nestings -- This is the same as above but sub products not rolled up (no QU conversion and column is_aggregated_amount = 0 here)
BEGIN SELECT
SELECT CASE WHEN(( pr.sub_product_id AS product_id,
SELECT 1 SUM(s.amount) AS amount,
FROM recipes_nestings_resolved rnr SUM(s.amount) AS amount_aggregated,
WHERE NEW.recipe_id = rnr.includes_recipe_id ROUND(SUM(IFNULL(s.price, 0) * s.amount), 2) AS value,
AND NEW.includes_recipe_id = rnr.recipe_id MIN(s.best_before_date) AS best_before_date,
) NOTNULL) THEN RAISE(ABORT, "Recursive nested recipe detected") END; IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened,
END; 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
CREATE TRIGGER prevent_infinite_nested_recipes_UPD BEFORE UPDATE ON recipes_nestings FROM products_resolved pr
BEGIN JOIN stock s
SELECT CASE WHEN(( ON pr.sub_product_id = s.product_id
SELECT 1 WHERE pr.parent_product_id != pr.sub_product_id
FROM recipes_nestings_resolved rnr GROUP BY pr.sub_product_id
WHERE NEW.recipe_id = rnr.includes_recipe_id HAVING SUM(s.amount) > 0;
AND NEW.includes_recipe_id = rnr.recipe_id
) NOTNULL) THEN RAISE(ABORT, "Recursive nested recipe detected") END;
END;

View File

@@ -1,46 +1,22 @@
DROP VIEW stock_current; CREATE VIEW product_price_history
CREATE VIEW stock_current
AS AS
SELECT SELECT
pr.parent_product_id AS product_id, sl.product_id AS id, -- Dummy, LessQL needs an id column
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id), 0) AS amount, sl.product_id,
SUM(s.amount * IFNULL(qucr.factor, 1.0)) AS amount_aggregated, sl.price,
IFNULL(ROUND((SELECT SUM(IFNULL(price,0) * amount) FROM stock WHERE product_id = pr.parent_product_id), 2), 0) AS value, sl.purchased_date,
MIN(s.best_before_date) AS best_before_date, sl.shopping_location_id
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id AND open = 1), 0) AS amount_opened, FROM stock_log sl
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, WHERE sl.transaction_type IN ('purchase', 'inventory-correction', 'stock-edit-new')
CASE WHEN p_sub.parent_product_id IS NOT NULL THEN 1 ELSE 0 END AS is_aggregated_amount AND sl.undone = 0
FROM products_resolved pr AND sl.price IS NOT NULL
JOIN stock s AND sl.id NOT IN (
ON pr.sub_product_id = s.product_id -- These are edited purchase and inventory-correction rows
JOIN products p_parent SELECT sl_origin.id
ON pr.parent_product_id = p_parent.id FROM stock_log sl_origin
AND p_parent.active = 1 JOIN stock_log sl_edit
JOIN products p_sub ON sl_origin.stock_id = sl_edit.stock_id
ON pr.sub_product_id = p_sub.id AND sl_edit.transaction_type = 'stock-edit-new'
AND p_sub.active = 1 AND sl_edit.id > sl_origin.id
LEFT JOIN quantity_unit_conversions_resolved qucr WHERE sl_origin.transaction_type IN ('purchase', 'inventory-correction')
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,
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;

View File

@@ -1,22 +1,134 @@
CREATE VIEW product_price_history CREATE TABLE user_permissions
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
permission_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
UNIQUE (user_id, permission_id)
);
CREATE TABLE permission_hierarchy
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
parent INTEGER NULL -- If the user has the parent permission, the user also has the child permission
);
-- The root/ADMIN permission
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('ADMIN', NULL);
-- User add/edit/read permissions
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_CREATE', (SELECT id FROM permission_hierarchy WHERE name = 'USERS'));
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_EDIT', last_insert_rowid());
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_READ', last_insert_rowid()),
('USERS_EDIT_SELF', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- Base permissions per major feature
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('STOCK', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('SHOPPINGLIST', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('RECIPES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('CHORES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('BATTERIES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('TASKS', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('EQUIPMENT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('CALENDAR', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- Sub feature permissions
INSERT INTO permission_hierarchy
(name, parent)
VALUES
-- Stock
('STOCK_PURCHASE', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_CONSUME', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_INVENTORY', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_TRANSFER', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_OPEN', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
-- Shopping list
('SHOPPINGLIST_ITEMS_ADD', (SELECT id FROM permission_hierarchy WHERE name = 'SHOPPINGLIST')),
('SHOPPINGLIST_ITEMS_DELETE', (SELECT id FROM permission_hierarchy WHERE name = 'SHOPPINGLIST')),
-- Recipes
('RECIPES_MEALPLAN', (SELECT id FROM permission_hierarchy WHERE name = 'RECIPES')),
-- Chores
('CHORE_TRACK_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'CHORES')),
('CHORE_UNDO_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'CHORES')),
-- Batteries
('BATTERIES_TRACK_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'BATTERIES')),
('BATTERIES_UNDO_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'BATTERIES')),
-- Tasks
('TASKS_UNDO_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'TASKS')),
('TASKS_MARK_COMPLETED', (SELECT id FROM permission_hierarchy WHERE name = 'TASKS')),
-- Others
('MASTER_DATA_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- All existing users get the ADMIN permission
INSERT INTO user_permissions
(permission_id, user_id)
SELECT (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'), id
FROM users;
CREATE VIEW permission_tree
AS
WITH RECURSIVE perm AS (
SELECT id AS root, id AS child, name, parent
FROM permission_hierarchy
UNION
SELECT perm.root, ph.id, ph.name, ph.id
FROM permission_hierarchy ph, perm
WHERE ph.parent = perm.child
)
SELECT root AS id, name AS name
FROM perm;
CREATE VIEW user_permissions_resolved
AS AS
SELECT SELECT
sl.product_id AS id, -- Dummy, LessQL needs an id column u.id AS id, -- Dummy for LessQL
sl.product_id, u.id AS user_id,
sl.price, pt.name AS permission_name
sl.purchased_date, FROM permission_tree pt, users u
sl.shopping_location_id WHERE pt.id IN (SELECT permission_id FROM user_permissions sub_up WHERE sub_up.user_id = u.id);
FROM stock_log sl
WHERE sl.transaction_type IN ('purchase', 'inventory-correction', 'stock-edit-new') CREATE VIEW uihelper_user_permissions
AND sl.undone = 0 AS
AND sl.price IS NOT NULL SELECT
AND sl.id NOT IN ( ph.id AS id,
-- These are edited purchase and inventory-correction rows u.id AS user_id,
SELECT sl_origin.id ph.name AS permission_name,
FROM stock_log sl_origin ph.id AS permission_id,
JOIN stock_log sl_edit (ph.name IN (
ON sl_origin.stock_id = sl_edit.stock_id SELECT pc.permission_name
AND sl_edit.transaction_type = 'stock-edit-new' FROM user_permissions_resolved pc
AND sl_edit.id > sl_origin.id WHERE pc.user_id = u.id
WHERE sl_origin.transaction_type IN ('purchase', 'inventory-correction') )
); ) AS has_permission,
ph.parent AS parent
FROM users u, permission_hierarchy ph;

View File

@@ -1,134 +1,16 @@
CREATE TABLE user_permissions DELETE FROM userfield_values
( WHERE IFNULL(value, '') = '';
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
permission_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
UNIQUE (user_id, permission_id) CREATE TRIGGER prevent_empty_userfields_INS AFTER INSERT ON userfield_values
); BEGIN
DELETE FROM userfield_values
WHERE id = NEW.id
AND IFNULL(value, '') = '';
END;
CREATE TABLE permission_hierarchy CREATE TRIGGER prevent_empty_userfields_UPD AFTER UPDATE ON userfield_values
( BEGIN
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, DELETE FROM userfield_values
name TEXT NOT NULL UNIQUE, WHERE id = NEW.id
parent INTEGER NULL -- If the user has the parent permission, the user also has the child permission AND IFNULL(value, '') = '';
); END;
-- The root/ADMIN permission
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('ADMIN', NULL);
-- User add/edit/read permissions
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_CREATE', (SELECT id FROM permission_hierarchy WHERE name = 'USERS'));
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_EDIT', last_insert_rowid());
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('USERS_READ', last_insert_rowid()),
('USERS_EDIT_SELF', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- Base permissions per major feature
INSERT INTO permission_hierarchy
(name, parent)
VALUES
('STOCK', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('SHOPPINGLIST', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('RECIPES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('CHORES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('BATTERIES', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('TASKS', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('EQUIPMENT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')),
('CALENDAR', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- Sub feature permissions
INSERT INTO permission_hierarchy
(name, parent)
VALUES
-- Stock
('STOCK_PURCHASE', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_CONSUME', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_INVENTORY', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_TRANSFER', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_OPEN', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
('STOCK_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'STOCK')),
-- Shopping list
('SHOPPINGLIST_ITEMS_ADD', (SELECT id FROM permission_hierarchy WHERE name = 'SHOPPINGLIST')),
('SHOPPINGLIST_ITEMS_DELETE', (SELECT id FROM permission_hierarchy WHERE name = 'SHOPPINGLIST')),
-- Recipes
('RECIPES_MEALPLAN', (SELECT id FROM permission_hierarchy WHERE name = 'RECIPES')),
-- Chores
('CHORE_TRACK_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'CHORES')),
('CHORE_UNDO_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'CHORES')),
-- Batteries
('BATTERIES_TRACK_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'BATTERIES')),
('BATTERIES_UNDO_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'BATTERIES')),
-- Tasks
('TASKS_UNDO_EXECUTION', (SELECT id FROM permission_hierarchy WHERE name = 'TASKS')),
('TASKS_MARK_COMPLETED', (SELECT id FROM permission_hierarchy WHERE name = 'TASKS')),
-- Others
('MASTER_DATA_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'));
-- All existing users get the ADMIN permission
INSERT INTO user_permissions
(permission_id, user_id)
SELECT (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'), id
FROM users;
CREATE VIEW permission_tree
AS
WITH RECURSIVE perm AS (
SELECT id AS root, id AS child, name, parent
FROM permission_hierarchy
UNION
SELECT perm.root, ph.id, ph.name, ph.id
FROM permission_hierarchy ph, perm
WHERE ph.parent = perm.child
)
SELECT root AS id, name AS name
FROM perm;
CREATE VIEW user_permissions_resolved
AS
SELECT
u.id AS id, -- Dummy for LessQL
u.id AS user_id,
pt.name AS permission_name
FROM permission_tree pt, users u
WHERE pt.id IN (SELECT permission_id FROM user_permissions sub_up WHERE sub_up.user_id = u.id);
CREATE VIEW uihelper_user_permissions
AS
SELECT
ph.id AS id,
u.id AS user_id,
ph.name AS permission_name,
ph.id AS permission_id,
(ph.name IN (
SELECT pc.permission_name
FROM user_permissions_resolved pc
WHERE pc.user_id = u.id
)
) AS has_permission,
ph.parent AS parent
FROM users u, permission_hierarchy ph;

View File

@@ -1,16 +1,36 @@
DELETE FROM userfield_values DROP VIEW userfield_values_resolved;
WHERE IFNULL(value, '') = ''; CREATE VIEW userfield_values_resolved
AS
SELECT
u.id, -- Dummy, LessQL needs an id column
u.entity,
u.name,
u.caption,
u.type,
u.show_as_column_in_tables,
u.row_created_timestamp,
u.config,
uv.object_id,
uv.value
FROM userfields u
JOIN userfield_values uv
ON u.id = uv.field_id
CREATE TRIGGER prevent_empty_userfields_INS AFTER INSERT ON userfield_values UNION
BEGIN
DELETE FROM userfield_values
WHERE id = NEW.id
AND IFNULL(value, '') = '';
END;
CREATE TRIGGER prevent_empty_userfields_UPD AFTER UPDATE ON userfield_values -- Kind of a hack, include userentity userfields also for the table userobjects
BEGIN SELECT
DELETE FROM userfield_values u.id, -- Dummy, LessQL needs an id column,
WHERE id = NEW.id 'userobjects',
AND IFNULL(value, '') = ''; u.name,
END; u.caption,
u.type,
u.show_as_column_in_tables,
u.row_created_timestamp,
u.config,
uv.object_id,
uv.value
FROM userfields u
JOIN userfield_values uv
ON u.id = uv.field_id
WHERE u.entity like 'userentity-%';

View File

@@ -1,36 +1,92 @@
DROP VIEW userfield_values_resolved; CREATE VIEW users_dto
CREATE VIEW userfield_values_resolved
AS AS
SELECT SELECT
u.id, -- Dummy, LessQL needs an id column id,
u.entity, username,
u.name, first_name,
u.caption, last_name,
u.type, row_created_timestamp,
u.show_as_column_in_tables, (CASE
u.row_created_timestamp, WHEN IFNULL(first_name, '') = '' AND IFNULL(last_name, '') != '' THEN last_name
u.config, WHEN IFNULL(last_name, '') = '' AND IFNULL(first_name, '') != '' THEN first_name
uv.object_id, WHEN IFNULL(last_name, '') != '' AND IFNULL(first_name, '') != '' THEN first_name || ' ' || last_name
uv.value ELSE username
FROM userfields u END
JOIN userfield_values uv ) AS display_name
ON u.id = uv.field_id FROM users;
UNION DROP VIEW chores_current;
CREATE VIEW chores_current
-- Kind of a hack, include userentity userfields also for the table userobjects AS
SELECT SELECT
u.id, -- Dummy, LessQL needs an id column, x.chore_id AS id, -- Dummy, LessQL needs an id column
'userobjects', x.chore_id,
u.name, x.chore_name,
u.caption, x.last_tracked_time,
u.type, CASE WHEN x.rollover = 1 AND DATETIME('now', 'localtime') > x.next_estimated_execution_time THEN
u.show_as_column_in_tables, DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' ' || STRFTIME('%H:%M:%S', x.next_estimated_execution_time))
u.row_created_timestamp, ELSE
u.config, x.next_estimated_execution_time
uv.object_id, END AS next_estimated_execution_time,
uv.value x.track_date_only,
FROM userfields u x.next_execution_assigned_to_user_id
JOIN userfield_values uv FROM (
ON u.id = uv.field_id
WHERE u.entity like 'userentity-%'; SELECT
h.id AS chore_id,
h.name AS chore_name,
MAX(l.tracked_time) AS last_tracked_time,
CASE h.period_type
WHEN 'manually' THEN '2999-12-31 23:59:59'
WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' day')
WHEN 'weekly' THEN (
SELECT next
FROM (
SELECT 'sunday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 0') AS next
UNION
SELECT 'monday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 1') AS next
UNION
SELECT 'tuesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 2') AS next
UNION
SELECT 'wednesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 3') AS next
UNION
SELECT 'thursday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 4') AS next
UNION
SELECT 'friday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 5') AS next
UNION
SELECT 'saturday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 6') AS next
)
WHERE INSTR(period_config, day) > 0
ORDER BY next
LIMIT 1
)
WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day')
WHEN 'yearly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' years')
END AS next_estimated_execution_time,
h.track_date_only,
h.rollover,
h.next_execution_assigned_to_user_id
FROM chores h
LEFT JOIN chores_log l
ON h.id = l.chore_id
AND l.undone = 0
GROUP BY h.id, h.name, h.period_days
) x;
DROP VIEW batteries_current;
CREATE VIEW batteries_current
AS
SELECT
b.id, -- Dummy, LessQL needs an id column
b.id AS battery_id,
MAX(l.tracked_time) AS last_tracked_time,
CASE WHEN b.charge_interval_days = 0
THEN '2999-12-31 23:59:59'
ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day')
END AS next_estimated_charge_time
FROM batteries b
LEFT JOIN battery_charge_cycles l
ON b.id = l.battery_id
GROUP BY b.id, b.charge_interval_days;

View File

@@ -1,92 +1,51 @@
CREATE VIEW users_dto ALTER TABLE stock_log
ADD user_id INTEGER NOT NULL DEFAULT 1;
CREATE VIEW uihelper_stock_journal
AS AS
SELECT SELECT
id, sl.id,
username, sl.row_created_timestamp,
first_name, sl.correlation_id,
last_name, sl.undone,
row_created_timestamp, sl.undone_timestamp,
(CASE sl.row_created_timestamp,
WHEN IFNULL(first_name, '') = '' AND IFNULL(last_name, '') != '' THEN last_name sl.transaction_type,
WHEN IFNULL(last_name, '') = '' AND IFNULL(first_name, '') != '' THEN first_name sl.spoiled,
WHEN IFNULL(last_name, '') != '' AND IFNULL(first_name, '') != '' THEN first_name || ' ' || last_name sl.amount,
ELSE username sl.location_id,
END l.name AS location_name,
) AS display_name p.name AS product_name,
FROM users; qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
u.display_name AS user_display_name
FROM stock_log sl
JOIN users_dto u
ON sl.user_id = u.id
JOIN products p
ON sl.product_id = p.id
JOIN locations l
ON p.location_id = l.id
JOIN quantity_units qu
ON p.qu_id_stock = qu.id;
DROP VIEW chores_current; CREATE VIEW uihelper_stock_journal_summary
CREATE VIEW chores_current
AS AS
SELECT SELECT
x.chore_id AS id, -- Dummy, LessQL needs an id column user_id AS id, -- Dummy, LessQL needs an id column
x.chore_id, user_id, u.display_name AS user_display_name,
x.chore_name, p.name AS product_name,
x.last_tracked_time, product_id,
CASE WHEN x.rollover = 1 AND DATETIME('now', 'localtime') > x.next_estimated_execution_time THEN transaction_type,
DATETIME(STRFTIME('%Y-%m-%d', DATETIME('now', 'localtime')) || ' ' || STRFTIME('%H:%M:%S', x.next_estimated_execution_time)) qu.name AS qu_name,
ELSE qu.name_plural AS qu_name_plural,
x.next_estimated_execution_time SUM(amount) AS amount
END AS next_estimated_execution_time, FROM stock_log sl
x.track_date_only, JOIN users_dto u
x.next_execution_assigned_to_user_id on sl.user_id = u.id
FROM ( JOIN products p
ON sl.product_id = p.id
SELECT JOIN quantity_units qu
h.id AS chore_id, ON p.qu_id_stock = qu.id
h.name AS chore_name, WHERE undone = 0
MAX(l.tracked_time) AS last_tracked_time, GROUP BY user_id, product_id, transaction_type;
CASE h.period_type
WHEN 'manually' THEN '2999-12-31 23:59:59'
WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' day')
WHEN 'weekly' THEN (
SELECT next
FROM (
SELECT 'sunday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 0') AS next
UNION
SELECT 'monday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 1') AS next
UNION
SELECT 'tuesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 2') AS next
UNION
SELECT 'wednesday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 3') AS next
UNION
SELECT 'thursday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 4') AS next
UNION
SELECT 'friday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 5') AS next
UNION
SELECT 'saturday' AS day, DATETIME(COALESCE((SELECT tracked_time FROM chores_log WHERE chore_id = h.id ORDER BY tracked_time DESC LIMIT 1), DATETIME('now', 'localtime')), '1 days', '+' || CAST((h.period_interval - 1) * 7 AS TEXT) || ' days', 'weekday 6') AS next
)
WHERE INSTR(period_config, day) > 0
ORDER BY next
LIMIT 1
)
WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day')
WHEN 'yearly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+' || CAST(h.period_interval AS TEXT) || ' years')
END AS next_estimated_execution_time,
h.track_date_only,
h.rollover,
h.next_execution_assigned_to_user_id
FROM chores h
LEFT JOIN chores_log l
ON h.id = l.chore_id
AND l.undone = 0
GROUP BY h.id, h.name, h.period_days
) x;
DROP VIEW batteries_current;
CREATE VIEW batteries_current
AS
SELECT
b.id, -- Dummy, LessQL needs an id column
b.id AS battery_id,
MAX(l.tracked_time) AS last_tracked_time,
CASE WHEN b.charge_interval_days = 0
THEN '2999-12-31 23:59:59'
ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day')
END AS next_estimated_charge_time
FROM batteries b
LEFT JOIN battery_charge_cycles l
ON b.id = l.battery_id
GROUP BY b.id, b.charge_interval_days;

View File

@@ -1,51 +1 @@
ALTER TABLE stock_log update user_settings set key = "stock_expiring_soon_days" where key = "stock_expring_soon_days";
ADD user_id INTEGER NOT NULL DEFAULT 1;
CREATE VIEW uihelper_stock_journal
AS
SELECT
sl.id,
sl.row_created_timestamp,
sl.correlation_id,
sl.undone,
sl.undone_timestamp,
sl.row_created_timestamp,
sl.transaction_type,
sl.spoiled,
sl.amount,
sl.location_id,
l.name AS location_name,
p.name AS product_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
u.display_name AS user_display_name
FROM stock_log sl
JOIN users_dto u
ON sl.user_id = u.id
JOIN products p
ON sl.product_id = p.id
JOIN locations l
ON p.location_id = l.id
JOIN quantity_units qu
ON p.qu_id_stock = qu.id;
CREATE VIEW uihelper_stock_journal_summary
AS
SELECT
user_id AS id, -- Dummy, LessQL needs an id column
user_id, u.display_name AS user_display_name,
p.name AS product_name,
product_id,
transaction_type,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
SUM(amount) AS amount
FROM stock_log sl
JOIN users_dto u
on sl.user_id = u.id
JOIN products p
ON sl.product_id = p.id
JOIN quantity_units qu
ON p.qu_id_stock = qu.id
WHERE undone = 0
GROUP BY user_id, product_id, transaction_type;

View File

@@ -1 +1,71 @@
update user_settings set key = "stock_expiring_soon_days" where key = "stock_expring_soon_days"; DROP VIEW quantity_unit_conversions_resolved;
CREATE VIEW quantity_unit_conversions_resolved
AS
-- First: Product "purchase to stock" conversion factor
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_purchase AS from_qu_id,
qu_from.name AS from_qu_name,
p.qu_id_stock AS to_qu_id,
qu_to.name AS to_qu_name,
p.qu_factor_purchase_to_stock AS factor
FROM products p
JOIN quantity_units qu_from
ON p.qu_id_purchase = qu_from.id
JOIN quantity_units qu_to
ON p.qu_id_stock = qu_to.id
UNION -- Inversed
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_stock AS from_qu_id,
qu_to.name AS from_qu_name,
p.qu_id_purchase AS to_qu_id,
qu_from.name AS to_qu_name,
1 / p.qu_factor_purchase_to_stock AS factor
FROM products p
JOIN quantity_units qu_from
ON p.qu_id_purchase = qu_from.id
JOIN quantity_units qu_to
ON p.qu_id_stock = qu_to.id
UNION
-- Second: Product specific overrides
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
quc.from_qu_id AS from_qu_id,
qu_from.name AS from_qu_name,
quc.to_qu_id AS to_qu_id,
qu_to.name AS to_qu_name,
quc.factor AS factor
FROM products p
JOIN quantity_unit_conversions quc
ON p.id = quc.product_id
JOIN quantity_units qu_from
ON quc.from_qu_id = qu_from.id
JOIN quantity_units qu_to
ON quc.to_qu_id = qu_to.id
UNION
-- Third: Default quantity unit conversion factors
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_stock AS from_qu_id,
qu_from.name AS from_qu_name,
quc.to_qu_id AS to_qu_id,
qu_to.name AS to_qu_name,
quc.factor AS factor
FROM products p
JOIN quantity_unit_conversions quc
ON p.qu_id_stock = quc.from_qu_id
AND quc.product_id IS NULL
JOIN quantity_units qu_from
ON quc.from_qu_id = qu_from.id
JOIN quantity_units qu_to
ON quc.to_qu_id = qu_to.id;

View File

@@ -1,71 +0,0 @@
DROP VIEW quantity_unit_conversions_resolved;
CREATE VIEW quantity_unit_conversions_resolved
AS
-- First: Product "purchase to stock" conversion factor
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_purchase AS from_qu_id,
qu_from.name AS from_qu_name,
p.qu_id_stock AS to_qu_id,
qu_to.name AS to_qu_name,
p.qu_factor_purchase_to_stock AS factor
FROM products p
JOIN quantity_units qu_from
ON p.qu_id_purchase = qu_from.id
JOIN quantity_units qu_to
ON p.qu_id_stock = qu_to.id
UNION -- Inversed
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_stock AS from_qu_id,
qu_to.name AS from_qu_name,
p.qu_id_purchase AS to_qu_id,
qu_from.name AS to_qu_name,
1 / p.qu_factor_purchase_to_stock AS factor
FROM products p
JOIN quantity_units qu_from
ON p.qu_id_purchase = qu_from.id
JOIN quantity_units qu_to
ON p.qu_id_stock = qu_to.id
UNION
-- Second: Product specific overrides
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
quc.from_qu_id AS from_qu_id,
qu_from.name AS from_qu_name,
quc.to_qu_id AS to_qu_id,
qu_to.name AS to_qu_name,
quc.factor AS factor
FROM products p
JOIN quantity_unit_conversions quc
ON p.id = quc.product_id
JOIN quantity_units qu_from
ON quc.from_qu_id = qu_from.id
JOIN quantity_units qu_to
ON quc.to_qu_id = qu_to.id
UNION
-- Third: Default quantity unit conversion factors
SELECT
p.id AS id, -- Dummy, LessQL needs an id column
p.id AS product_id,
p.qu_id_stock AS from_qu_id,
qu_from.name AS from_qu_name,
quc.to_qu_id AS to_qu_id,
qu_to.name AS to_qu_name,
quc.factor AS factor
FROM products p
JOIN quantity_unit_conversions quc
ON p.qu_id_stock = quc.from_qu_id
AND quc.product_id IS NULL
JOIN quantity_units qu_from
ON quc.from_qu_id = qu_from.id
JOIN quantity_units qu_to
ON quc.to_qu_id = qu_to.id;