diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js index 5dddbb01..c063d7a8 100644 --- a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js @@ -96,6 +96,8 @@ const PLACEHOLDERS = [ ['[[AMOUNT]]', 'Lot amount'], ['[[LOCATION]]', 'Storage location'], ['[[LOCATION_FULL]]', 'Storage location (Full path)'], + ['[[OWNER]]', 'Full name of the lot owner'], + ['[[OWNER_USERNAME]]', 'Username of the lot owner'], ] }, { diff --git a/assets/ckeditor/plugins/PartDBLabel/lang/de.js b/assets/ckeditor/plugins/PartDBLabel/lang/de.js index d49a26ed..c263825f 100644 --- a/assets/ckeditor/plugins/PartDBLabel/lang/de.js +++ b/assets/ckeditor/plugins/PartDBLabel/lang/de.js @@ -55,6 +55,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, { 'Lot amount': 'Lot Menge', 'Storage location': 'Lagerort', 'Storage location (Full path)': 'Lagerort (Vollständiger Pfad)', + 'Full name of the lot owner': 'Name des Besitzers des Lots', + 'Username of the lot owner': 'Benutzername des Besitzers des Lots', 'Barcodes': 'Barcodes', diff --git a/migrations/Version20230219225340.php b/migrations/Version20230219225340.php index cf22d3be..94e08963 100644 --- a/migrations/Version20230219225340.php +++ b/migrations/Version20230219225340.php @@ -281,7 +281,7 @@ final class Version20230219225340 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); $this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachement, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM projects'); $this->addSql('DROP TABLE projects'); - $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, order_only_missing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, description DEFAULT \'\', CONSTRAINT FK_11074E9A727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, order_only_missing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, description CLOB DEFAULT \'\', CONSTRAINT FK_11074E9A727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); $this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description) SELECT id, parent_id, id_preview_attachement, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM __temp__projects'); $this->addSql('DROP TABLE __temp__projects'); $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); diff --git a/migrations/Version20230402170923.php b/migrations/Version20230402170923.php new file mode 100644 index 00000000..e1ad8adb --- /dev/null +++ b/migrations/Version20230402170923.php @@ -0,0 +1,300 @@ +addSql('ALTER TABLE part_lots ADD id_owner INT DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES `users` (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id)'); + $this->addSql('ALTER TABLE storelocations ADD id_owner INT DEFAULT NULL, ADD part_owner_must_match TINYINT(1) DEFAULT 0 NOT NULL'); + $this->addSql('ALTER TABLE storelocations ADD CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES `users` (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_751702021E5A74C ON storelocations (id_owner)'); + $this->addSql('ALTER TABLE users ADD about_me LONGTEXT DEFAULT \'\' NOT NULL'); + + $this->addSql('ALTER TABLE projects CHANGE description description LONGTEXT DEFAULT \'\' NOT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE log CHANGE level level TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE part_lots DROP FOREIGN KEY FK_EBC8F94321E5A74C'); + $this->addSql('DROP INDEX IDX_EBC8F94321E5A74C ON part_lots'); + $this->addSql('ALTER TABLE part_lots DROP id_owner'); + $this->addSql('ALTER TABLE projects DROP FOREIGN KEY FK_5C93B3A4727ACA70'); + $this->addSql('ALTER TABLE `storelocations` DROP FOREIGN KEY FK_751702021E5A74C'); + $this->addSql('DROP INDEX IDX_751702021E5A74C ON `storelocations`'); + $this->addSql('ALTER TABLE `storelocations` DROP id_owner, DROP part_owner_must_match'); + $this->addSql('ALTER TABLE `users` DROP about_me'); + } + + public function sqLiteUp(Schema $schema): void + { + //In Version20230219225340 the type of project description was set to an empty string, which caused problems with doctrine migrations, fix it here + $this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM projects'); + $this->addSql('DROP TABLE projects'); + $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, order_only_missing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, description CLOB DEFAULT \'\', CONSTRAINT FK_11074E9A727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description) SELECT id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM __temp__projects'); + $this->addSql('DROP TABLE __temp__projects'); + $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); + $this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)'); + + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM groups'); + $this->addSql('DROP TABLE groups'); + $this->addSql('CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB DEFAULT \'[]\' NOT NULL --(DC2Type:json) + , CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO groups (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON groups (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON groups (parent_id)'); + $this->addSql('CREATE INDEX group_idx_name ON groups (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON groups (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, datetime DATETIME NOT NULL, level TINYINT(4) NOT NULL, target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL --(DC2Type:json) + , type SMALLINT NOT NULL, username VARCHAR(255) NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, id_user, datetime, level, target_id, target_type, extra, type, username) SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM pricedetails'); + $this->addSql('DROP TABLE pricedetails'); + $this->addSql('CREATE TABLE pricedetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL --(DC2Type:big_decimal) + , price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO pricedetails (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON pricedetails (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON pricedetails (min_discount_quantity)'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON pricedetails (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON pricedetails (orderdetails_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AFC547992F180363 FOREIGN KEY (id_device) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AFC54799C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM projects'); + $this->addSql('DROP TABLE projects'); + $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, order_only_missing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, description CLOB DEFAULT \'\' NOT NULL, CONSTRAINT FK_11074E9A727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description) SELECT id, parent_id, id_preview_attachment, order_quantity, order_only_missing_parts, comment, not_selectable, name, last_modified, datetime_added, status, description FROM __temp__projects'); + $this->addSql('DROP TABLE __temp__projects'); + $this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__storelocations AS SELECT id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added FROM storelocations'); + $this->addSql('DROP TABLE storelocations'); + $this->addSql('CREATE TABLE storelocations (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, storage_type_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_owner INTEGER DEFAULT NULL, is_full BOOLEAN NOT NULL, only_single_part BOOLEAN NOT NULL, limit_to_existing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, part_owner_must_match BOOLEAN DEFAULT 0 NOT NULL, CONSTRAINT FK_7517020727ACA70 FOREIGN KEY (parent_id) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020B270BFF1 FOREIGN KEY (storage_type_id) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO storelocations (id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added FROM __temp__storelocations'); + $this->addSql('DROP TABLE __temp__storelocations'); + $this->addSql('CREATE INDEX IDX_7517020EA7100A1 ON storelocations (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_7517020B270BFF1 ON storelocations (storage_type_id)'); + $this->addSql('CREATE INDEX IDX_7517020727ACA70 ON storelocations (parent_id)'); + $this->addSql('CREATE INDEX location_idx_name ON storelocations (name)'); + $this->addSql('CREATE INDEX location_idx_parent_name ON storelocations (parent_id, name)'); + $this->addSql('CREATE INDEX IDX_751702021E5A74C ON storelocations (id_owner)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM suppliers'); + $this->addSql('DROP TABLE suppliers'); + $this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)'); + $this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user FROM users'); + $this->addSql('DROP TABLE users'); + $this->addSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL --(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB DEFAULT \'[]\' NOT NULL --(DC2Type:json) + , saml_user BOOLEAN NOT NULL, about_me CLOB DEFAULT \'\' NOT NULL, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO users (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON users (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON users (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON users (group_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON users (name)'); + $this->addSql('CREATE INDEX user_idx_username ON users (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path) + , aaguid CLOB NOT NULL --(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL --(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM "groups"'); + $this->addSql('DROP TABLE "groups"'); + $this->addSql('CREATE TABLE "groups" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB DEFAULT \'[]\' NOT NULL -- +(DC2Type:json) + , CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "groups" (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON "groups" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON "groups" (id_preview_attachment)'); + $this->addSql('CREATE INDEX group_idx_name ON "groups" (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON "groups" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, username VARCHAR(255) NOT NULL, datetime DATETIME NOT NULL, level BOOLEAN NOT NULL, target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL -- +(DC2Type:json) + , type SMALLINT NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, id_user, username, datetime, level, target_id, target_type, extra, type) SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM "pricedetails"'); + $this->addSql('DROP TABLE "pricedetails"'); + $this->addSql('CREATE TABLE "pricedetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL -- +(DC2Type:big_decimal) + , price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES "orderdetails" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "pricedetails" (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON "pricedetails" (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON "pricedetails" (orderdetails_id)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON "pricedetails" (min_discount_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON "pricedetails" (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM projects'); + $this->addSql('DROP TABLE projects'); + $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts BOOLEAN NOT NULL, description CLOB DEFAULT \'\', comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM __temp__projects'); + $this->addSql('DROP TABLE __temp__projects'); + $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); + $this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__storelocations AS SELECT id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added FROM "storelocations"'); + $this->addSql('DROP TABLE "storelocations"'); + $this->addSql('CREATE TABLE "storelocations" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, storage_type_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, is_full BOOLEAN NOT NULL, only_single_part BOOLEAN NOT NULL, limit_to_existing_parts BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_7517020727ACA70 FOREIGN KEY (parent_id) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020B270BFF1 FOREIGN KEY (storage_type_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "storelocations" (id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, storage_type_id, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, comment, not_selectable, name, last_modified, datetime_added FROM __temp__storelocations'); + $this->addSql('DROP TABLE __temp__storelocations'); + $this->addSql('CREATE INDEX IDX_7517020727ACA70 ON "storelocations" (parent_id)'); + $this->addSql('CREATE INDEX IDX_7517020B270BFF1 ON "storelocations" (storage_type_id)'); + $this->addSql('CREATE INDEX IDX_7517020EA7100A1 ON "storelocations" (id_preview_attachment)'); + $this->addSql('CREATE INDEX location_idx_name ON "storelocations" (name)'); + $this->addSql('CREATE INDEX location_idx_parent_name ON "storelocations" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM "suppliers"'); + $this->addSql('DROP TABLE "suppliers"'); + $this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "suppliers" (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM "users"'); + $this->addSql('DROP TABLE "users"'); + $this->addSql('CREATE TABLE "users" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL -- +(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL -- +(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, saml_user BOOLEAN DEFAULT 0 NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB DEFAULT \'[]\' NOT NULL -- +(DC2Type:json) + , CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "users" (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); + $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL -- +(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL -- +(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL -- +(DC2Type:trust_path) + , aaguid CLOB NOT NULL -- +(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL -- +(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index c02a6b4f..f0d6fdfe 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -55,8 +55,11 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Contracts\Translation\TranslatorInterface; +use function Symfony\Component\Translation\t; + /** * @Route("/part") */ @@ -360,23 +363,28 @@ class PartController extends AbstractController $action = $request->request->get('action'); - - switch ($action) { - case "withdraw": - case "remove": - $this->denyAccessUnlessGranted('withdraw', $partLot); - $withdrawAddHelper->withdraw($partLot, $amount, $comment); - break; - case "add": - $this->denyAccessUnlessGranted('add', $partLot); - $withdrawAddHelper->add($partLot, $amount, $comment); - break; - case "move": - $this->denyAccessUnlessGranted('move', $partLot); - $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment); - break; - default: - throw new \RuntimeException("Unknown action!"); + try { + switch ($action) { + case "withdraw": + case "remove": + $this->denyAccessUnlessGranted('withdraw', $partLot); + $withdrawAddHelper->withdraw($partLot, $amount, $comment); + break; + case "add": + $this->denyAccessUnlessGranted('add', $partLot); + $withdrawAddHelper->add($partLot, $amount, $comment); + break; + case "move": + $this->denyAccessUnlessGranted('move', $partLot); + $this->denyAccessUnlessGranted('move', $targetLot); + $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment); + break; + default: + throw new \RuntimeException("Unknown action!"); + } + } catch (AccessDeniedException $exception) { + $this->addFlash('error', t('part.withdraw.access_denied')); + goto err; } //Save the changes to the DB @@ -387,6 +395,7 @@ class PartController extends AbstractController $this->addFlash('error', 'CSRF Token invalid!'); } + err: //If an redirect was passed, then redirect there if($request->request->get('_redirect')) { return $this->redirect($request->request->get('_redirect')); diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index 40db1fc2..155f844c 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -26,6 +26,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\UserSystem\User; use App\Validator\Constraints\Selectable; use App\Validator\Constraints\ValidPartLot; use DateTime; @@ -33,6 +34,7 @@ use Doctrine\ORM\Mapping as ORM; use Exception; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * This entity describes a lot where parts can be stored. @@ -111,6 +113,13 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named */ protected Part $part; + /** + * @var User|null The owner of this part lot + * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") + * @ORM\JoinColumn(name="id_owner", referencedColumnName="id", nullable=true, onDelete="SET NULL") + */ + protected ?User $owner; + public function __clone() { if ($this->id) { @@ -304,8 +313,46 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named return $this; } + /** + * Returns the owner of this part lot. + * @return User|null + */ + public function getOwner(): ?User + { + return $this->owner; + } + + /** + * Sets the owner of this part lot. + * @param User|null $owner + * @return PartLot + */ + public function setOwner(?User $owner): PartLot + { + $this->owner = $owner; + return $this; + } + public function getName(): string { return $this->description; } + + /** + * @Assert\Callback + */ + public function validate(ExecutionContextInterface $context, $payload) + { + //When the storage location sets the owner must match, the part lot owner must match the storage location owner + if ($this->getStorageLocation() && $this->getStorageLocation()->isPartOwnerMustMatch() + && $this->getStorageLocation()->getOwner() && $this->getOwner()) { + if ($this->getOwner() !== $this->getStorageLocation()->getOwner() + && $this->owner->getID() !== $this->getStorageLocation()->getOwner()->getID()) { + $context->buildViolation('validator.part_lot.owner_must_match_storage_location_owner') + ->setParameter('%owner_name%', $this->getStorageLocation()->getOwner()->getFullName(true)) + ->atPath('owner') + ->addViolation(); + } + } + } } diff --git a/src/Entity/Parts/Storelocation.php b/src/Entity/Parts/Storelocation.php index 52f84f2d..1aa0b2b5 100644 --- a/src/Entity/Parts/Storelocation.php +++ b/src/Entity/Parts/Storelocation.php @@ -25,6 +25,7 @@ namespace App\Entity\Parts; use App\Entity\Attachments\StorelocationAttachment; use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Parameters\StorelocationParameter; +use App\Entity\UserSystem\User; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -89,6 +90,19 @@ class Storelocation extends AbstractPartsContainingDBElement */ protected bool $limit_to_existing_parts = false; + /** + * @var User|null The owner of this storage location + * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") + * @ORM\JoinColumn(name="id_owner", referencedColumnName="id", nullable=true, onDelete="SET NULL") + */ + protected ?User $owner; + + /** + * @var bool If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + * @ORM\Column(type="boolean", options={"default"=false}) + */ + protected bool $part_owner_must_match = false; + /** * @var Collection * @ORM\OneToMany(targetEntity="App\Entity\Attachments\StorelocationAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) @@ -166,6 +180,49 @@ class Storelocation extends AbstractPartsContainingDBElement return $this; } + /** + * Returns the owner of this storage location + * @return User|null + */ + public function getOwner(): ?User + { + return $this->owner; + } + + /** + * Sets the owner of this storage location + * @param User|null $owner + * @return Storelocation + */ + public function setOwner(?User $owner): Storelocation + { + $this->owner = $owner; + return $this; + } + + /** + * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + * @return bool + */ + public function isPartOwnerMustMatch(): bool + { + return $this->part_owner_must_match; + } + + /** + * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + * @param bool $part_owner_must_match + * @return Storelocation + */ + public function setPartOwnerMustMatch(bool $part_owner_must_match): Storelocation + { + $this->part_owner_must_match = $part_owner_must_match; + return $this; + } + + + + /******************************************************************************** * * Setters diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index bf0dbfb0..722c0387 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -88,7 +88,7 @@ class Project extends AbstractStructuralDBElement protected bool $order_only_missing_parts = false; /** - * @ORM\Column(type="text", nullable=false, columnDefinition="DEFAULT ''") + * @ORM\Column(type="text", nullable=false, options={"default" : ""}) * @Groups({"simple", "extended", "full"}) */ protected string $description = ''; diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index fd8c9054..a468d355 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -103,6 +103,13 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe */ protected string $instock_comment_w = ''; + /** + * @var string A self-description of the user + * @ORM\Column(type="text", options={"default": ""}) + * @Groups({"full", "import"}) + */ + protected string $aboutMe = ''; + /** @var int The version of the trusted device cookie. Used to invalidate all trusted device cookies at once. * @ORM\Column(type="integer") */ @@ -254,7 +261,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * @var DateTime the time until the password reset token is valid - * @ORM\Column(type="datetime", nullable=true, columnDefinition="DEFAULT NULL") + * @ORM\Column(type="datetime", nullable=true, options={"default": null}) */ protected $pw_reset_expires; @@ -625,6 +632,28 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } + /** + * Returns the about me text of the user. + * @return string + */ + public function getAboutMe(): string + { + return $this->aboutMe; + } + + /** + * Change the about me text of the user. + * @param string $aboutMe + * @return User + */ + public function setAboutMe(string $aboutMe): User + { + $this->aboutMe = $aboutMe; + return $this; + } + + + /** * Gets the language the user prefers (as 2 letter ISO code). * diff --git a/src/Form/AdminPages/StorelocationAdminForm.php b/src/Form/AdminPages/StorelocationAdminForm.php index 8a85e4ec..9e24dcd3 100644 --- a/src/Form/AdminPages/StorelocationAdminForm.php +++ b/src/Form/AdminPages/StorelocationAdminForm.php @@ -24,7 +24,9 @@ namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Parts\MeasurementUnit; +use App\Entity\UserSystem\User; use App\Form\Type\StructuralEntityType; +use App\Form\Type\UserSelectType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\FormBuilderInterface; @@ -63,5 +65,16 @@ class StorelocationAdminForm extends BaseEntityAdminForm 'disable_not_selectable' => true, 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + + $builder->add('owner', UserSelectType::class, [ + 'required' => false, + 'label' => 'storelocation.owner.label', + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); + $builder->add('part_owner_must_match', CheckboxType::class, [ + 'required' => false, + 'label' => 'storelocation.part_owner_must_match.label', + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); } } diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index 2e23c94a..2c46df1a 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -27,6 +27,7 @@ use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; use App\Form\Type\SIUnitType; use App\Form\Type\StructuralEntityType; +use App\Form\Type\UserSelectType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -98,6 +99,12 @@ class PartLotType extends AbstractType 'required' => false, 'empty_data' => '', ]); + + $builder->add('owner', UserSelectType::class, [ + 'label' => 'part_lot.owner', + 'required' => false, + 'help' => 'part_lot.owner.help', + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/Type/Helper/StructuralEntityChoiceHelper.php b/src/Form/Type/Helper/StructuralEntityChoiceHelper.php index db25e6da..6448a734 100644 --- a/src/Form/Type/Helper/StructuralEntityChoiceHelper.php +++ b/src/Form/Type/Helper/StructuralEntityChoiceHelper.php @@ -21,8 +21,13 @@ namespace App\Form\Type\Helper; use App\Entity\Attachments\AttachmentType; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Contracts\HasMasterAttachmentInterface; use App\Entity\PriceInformations\Currency; +use App\Entity\UserSystem\User; +use App\Form\Type\MasterPictureAttachmentType; use App\Services\Attachments\AttachmentURLGenerator; use RuntimeException; use Symfony\Component\Intl\Currencies; @@ -43,37 +48,49 @@ class StructuralEntityChoiceHelper /** * Generates the choice attributes for the given AbstractStructuralDBElement. - * @param AbstractStructuralDBElement $choice + * @param AbstractNamedDBElement $choice * @param Options|array $options * @return array|string[] */ - public function generateChoiceAttr(AbstractStructuralDBElement $choice, $options): array + public function generateChoiceAttr(AbstractNamedDBElement $choice, $options): array { - $tmp = []; - - //Disable attribute if the choice is marked as not selectable - if (($options['disable_not_selectable'] ?? false) && $choice->isNotSelectable()) { - $tmp += ['disabled' => 'disabled']; - } - - if ($choice instanceof AttachmentType) { - $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; - } - - $level = $choice->getLevel(); - /** @var AbstractStructuralDBElement|null $parent */ - $parent = $options['subentities_of'] ?? null; - if (null !== $parent) { - $level -= $parent->getLevel() - 1; - } - - $tmp += [ - 'data-level' => $level, - 'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null, - 'data-path' => $choice->getFullPath('->'), - 'data-image' => $choice->getMasterPictureAttachment() ? $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), 'thumbnail_xs') : null, + $tmp = [ + 'data-level' => 0, + 'data-path' => $choice->getName(), ]; + if ($choice instanceof AbstractStructuralDBElement) { + //Disable attribute if the choice is marked as not selectable + if (($options['disable_not_selectable'] ?? false) && $choice->isNotSelectable()) { + $tmp += ['disabled' => 'disabled']; + } + + if ($choice instanceof AttachmentType) { + $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; + } + + $level = $choice->getLevel(); + /** @var AbstractStructuralDBElement|null $parent */ + $parent = $options['subentities_of'] ?? null; + if (null !== $parent) { + $level -= $parent->getLevel() - 1; + } + + $tmp += [ + 'data-level' => $level, + 'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null, + 'data-path' => $choice->getFullPath('->'), + ]; + } + + if ($choice instanceof HasMasterAttachmentInterface) { + $tmp['data-image'] = $choice->getMasterPictureAttachment() ? + $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), + 'thumbnail_xs') + : null + ; + } + if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) { $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; } @@ -112,20 +129,20 @@ class StructuralEntityChoiceHelper /** * Returns the choice label for the given AbstractStructuralDBElement. - * @param AbstractStructuralDBElement $choice + * @param AbstractNamedDBElement $choice * @return string */ - public function generateChoiceLabel(AbstractStructuralDBElement $choice): string + public function generateChoiceLabel(AbstractNamedDBElement $choice): string { return $choice->getName(); } /** * Returns the choice value for the given AbstractStructuralDBElement. - * @param AbstractStructuralDBElement|null $element + * @param AbstractNamedDBElement|null $element * @return string|int|null */ - public function generateChoiceValue(?AbstractStructuralDBElement $element) + public function generateChoiceValue(?AbstractNamedDBElement $element) { if ($element === null) { return null; @@ -138,18 +155,21 @@ class StructuralEntityChoiceHelper * So please do not change this! */ if ($element->getID() === null) { - //Must be the same as the separator in the choice_loader, otherwise this will not work! - return $element->getFullPath('->'); + if ($element instanceof AbstractStructuralDBElement) { + //Must be the same as the separator in the choice_loader, otherwise this will not work! + return $element->getFullPath('->'); + } + return $element->getName(); } return $element->getID(); } /** - * @param AbstractStructuralDBElement $element + * @param AbstractDBElement $element * @return string|null */ - public function generateGroupBy(AbstractStructuralDBElement $element): ?string + public function generateGroupBy(AbstractDBElement $element): ?string { //Show entities that are not added to DB yet separately from other entities if ($element->getID() === null) { diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index 9c65a7ef..b58caef7 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Form\Type; use App\Entity\Attachments\AttachmentType; +use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Form\Type\Helper\StructuralEntityChoiceHelper; use App\Form\Type\Helper\StructuralEntityChoiceLoader; @@ -105,7 +106,7 @@ class StructuralEntityType extends AbstractType 'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext 'subentities_of' => null, //Only show entities with the given parent class 'disable_not_selectable' => false, //Disable entries with not selectable property - 'choice_value' => function (?AbstractStructuralDBElement $element) { + 'choice_value' => function (?AbstractNamedDBElement $element) { return $this->choice_helper->generateChoiceValue($element); }, //Use the element id as option value and for comparing items 'choice_loader' => function (Options $options) { @@ -121,7 +122,7 @@ class StructuralEntityType extends AbstractType return $this->choice_helper->generateChoiceAttr($choice, $options); }; }, - 'group_by' => function (AbstractStructuralDBElement $element) { + 'group_by' => function (AbstractNamedDBElement $element) { return $this->choice_helper->generateGroupBy($element); }, 'choice_translation_domain' => false, //Don't translate the entity names diff --git a/src/Form/Type/UserSelectType.php b/src/Form/Type/UserSelectType.php new file mode 100644 index 00000000..c9c0830c --- /dev/null +++ b/src/Form/Type/UserSelectType.php @@ -0,0 +1,47 @@ +. + */ + +namespace App\Form\Type; + +use App\Entity\UserSystem\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class UserSelectType extends AbstractType +{ + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'class' => User::class, + 'choice_label' => function (Options $options) { + return function (User $choice, $key, $value) { + return $choice->getFullName(true); + }; + }, + ]); + } + + public function getParent() + { + return StructuralEntityType::class; + } +} \ No newline at end of file diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index 380d8c8f..9f97d8f3 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -29,6 +29,7 @@ use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\MasterPictureAttachmentType; +use App\Form\Type\RichTextEditorType; use App\Form\Type\StructuralEntityType; use App\Form\Type\ThemeChoiceType; use Symfony\Component\Form\AbstractType; @@ -126,6 +127,16 @@ class UserAdminForm extends AbstractType 'required' => false, 'disabled' => !$this->security->isGranted('edit_infos', $entity), ]) + ->add('aboutMe', RichTextEditorType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'user.aboutMe.label', + 'attr' => [ + 'rows' => 4, + ], + 'mode' => 'markdown-full', + 'disabled' => !$this->security->isGranted('edit_infos', $entity), + ]) //Config section ->add('language', LanguageType::class, [ diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index 685ac14d..c54e90e9 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -24,6 +24,7 @@ namespace App\Form; use App\Entity\UserSystem\User; use App\Form\Type\CurrencyEntityType; +use App\Form\Type\RichTextEditorType; use App\Form\Type\ThemeChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Event\PreSetDataEvent; @@ -93,6 +94,16 @@ class UserSettingsType extends AbstractType ]), ], ]) + ->add('aboutMe', RichTextEditorType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'user.aboutMe.label', + 'attr' => [ + 'rows' => 4, + ], + 'mode' => 'markdown-full', + 'disabled' => !$this->security->isGranted('edit_infos', $options['data']) || $this->demo_mode, + ]) ->add('language', LanguageType::class, [ 'disabled' => $this->demo_mode, 'required' => false, diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php index aa89453d..8387d47c 100644 --- a/src/Repository/NamedDBElementRepository.php +++ b/src/Repository/NamedDBElementRepository.php @@ -59,4 +59,14 @@ class NamedDBElementRepository extends DBElementRepository return $result; } + + /** + * Returns the list of all nodes to use in a select box. + * @return AbstractNamedDBElement[] + */ + public function toNodesList(): array + { + //All nodes are sorted by name + return $this->findBy([], ['name' => 'ASC']); + } } diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index da05070b..0c70e629 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -67,7 +67,15 @@ class PartLotVoter extends ExtendedVoter if (in_array($attribute, ['withdraw', 'add', 'move'])) { - return $this->resolver->inherit($user, 'parts_stock', $attribute) ?? false; + $base_permission = $this->resolver->inherit($user, 'parts_stock', $attribute) ?? false; + + $lot_permission = true; + //If the lot has an owner, we need to check if the user is the owner of the lot to be allowed to withdraw it. + if ($subject instanceof PartLot && $subject->getOwner()) { + $lot_permission = $subject->getOwner() === $user || $subject->getOwner()->getID() === $user->getID(); + } + + return $base_permission && $lot_permission; } switch ($attribute) { diff --git a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php index 3f204f32..a4fdf32a 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php @@ -102,6 +102,14 @@ final class PartLotProvider implements PlaceholderProviderInterface return $label_target->getStorageLocation() ? $label_target->getStorageLocation()->getFullPath() : ''; } + if ('[[OWNER]]' === $placeholder) { + return $label_target->getOwner() ? $label_target->getOwner()->getFullName() : ''; + } + + if ('[[OWNER_USERNAME]]' === $placeholder) { + return $label_target->getOwner() ? $label_target->getOwner()->getUsername() : ''; + } + return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart()); } diff --git a/src/Services/Trees/NodesListBuilder.php b/src/Services/Trees/NodesListBuilder.php index ff8240e0..a7b70793 100644 --- a/src/Services/Trees/NodesListBuilder.php +++ b/src/Services/Trees/NodesListBuilder.php @@ -46,7 +46,7 @@ class NodesListBuilder } /** - * Gets a flattened hierachical tree. Useful for generating option lists. + * Gets a flattened hierarchical tree. Useful for generating option lists. * In difference to the Repository Function, the results here are cached. * * @param string $class_name the class name of the entity you want to retrieve @@ -66,6 +66,7 @@ class NodesListBuilder $item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]); /** @var StructuralDBElementRepository $repo */ $repo = $this->em->getRepository($class_name); + return $repo->toNodesList($parent); }); } diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index 5932b715..4741c02d 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -25,6 +25,8 @@ {{ form_row(form.is_full) }} {{ form_row(form.limit_to_existing_parts) }} {{ form_row(form.only_single_part) }} + {{ form_row(form.owner) }} + {{ form_row(form.part_owner_must_match) }} {% endblock %} diff --git a/templates/admin/user_admin.html.twig b/templates/admin/user_admin.html.twig index 5343377e..e24ce36d 100644 --- a/templates/admin/user_admin.html.twig +++ b/templates/admin/user_admin.html.twig @@ -22,6 +22,7 @@ {{ form_row(form.last_name) }} {{ form_row(form.email) }} {{ form_row(form.department) }} + {{ form_row(form.aboutMe) }} {% endblock %} {% block additional_panes %} @@ -33,9 +34,11 @@
-
- {% trans %}user.saml_user{% endtrans %} -
+ {% if entity.samlUser %} +
+ {% trans %}user.saml_user{% endtrans %} +
+ {% endif %} {{ form_row(form.new_password) }} {{ form_row(form.need_pw_change) }} diff --git a/templates/helper.twig b/templates/helper.twig index 1957864f..137dd792 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -159,11 +159,7 @@ {% endif %} {% if user is not null %} - {% if user.fullName is not empty %} - ({{ _self.user_icon(user) }} {{ user.fullName }}) - {% else %} - ({{ _self.user_icon(user) }} @{{ user.name }}) - {% endif %} + ({{ _self.user_icon_link(user) }}) {% endif %} {% endif %} {% endmacro %} @@ -180,6 +176,14 @@ User avatar {% endmacro %} +{% macro user_icon_link(user) %} + {% if user.fullName is not empty %} + {{ _self.user_icon(user) }} {{ user.fullName }} + {% else %} + {{ _self.user_icon(user) }} @{{ user.name }} + {% endif %} +{% endmacro %} + {% macro entity_preview_sm(entity) %} {# @var entity \App\Entity\Contracts\HasMasterAttachmentInterface #} {% if entity.masterPictureAttachment and entity.masterPictureAttachment.picture and attachment_manager.fileExisting(entity.masterPictureAttachment) %} diff --git a/templates/parts/info/_part_lots.html.twig b/templates/parts/info/_part_lots.html.twig index e6d8637f..1d11b1a4 100644 --- a/templates/parts/info/_part_lots.html.twig +++ b/templates/parts/info/_part_lots.html.twig @@ -41,21 +41,26 @@
+ {% if lot.owner %} + + {{ helper.user_icon_link(lot.owner) }} +
+ {% endif %} {% if lot.expirationDate %} - - {{ lot.expirationDate | format_date() }} + + {{ lot.expirationDate | format_date() }}
{% endif %} {% if lot.expired %}
- + {% trans %}part_lots.is_expired{% endtrans %} {% endif %} {% if lot.needsRefill %}
- + {% trans %}part_lots.need_refill{% endtrans %} diff --git a/templates/users/user_info.html.twig b/templates/users/user_info.html.twig index fd5a6374..28d720b6 100644 --- a/templates/users/user_info.html.twig +++ b/templates/users/user_info.html.twig @@ -64,6 +64,11 @@ {% endif %}
+ {% if user.aboutMe is not empty %} +
+
{% trans %}user.aboutMe.label{% endtrans %}
+ {{ user.aboutMe | format_markdown }} + {% endif %} {% endblock %} diff --git a/templates/users/user_settings.html.twig b/templates/users/user_settings.html.twig index b67708b9..cdd419bc 100644 --- a/templates/users/user_settings.html.twig +++ b/templates/users/user_settings.html.twig @@ -34,6 +34,7 @@ {{ form_widget(settings_form.remove_avatar) }} + {{ form_row(settings_form.aboutMe) }}
{{ form_row(settings_form.language) }} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f5140d56..9bcc89c8 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11199,5 +11199,59 @@ Element 3 Edit Measurement Unit + + + user.aboutMe.label + About Me + + + + + storelocation.owner.label + Owner + + + + + storelocation.part_owner_must_match.label + Part Lot owner must match storage location owner + + + + + part_lot.owner + Owner + + + + + part_lot.owner.help + Only the owner can withdraw or add stock to this lot. + + + + + log.element_edited.changed_fields.owner + Owner + + + + + log.element_edited.changed_fields.instock_unknown + Amount unknown + + + + + log.element_edited.changed_fields.needs_refill + Refill needed + + + + + part.withdraw.access_denied + Not allowed to do the desired action. Please check your permissions and the owner of the part lots. + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 7150f094..771f3163 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -305,5 +305,11 @@ Set a value here, or upload a file to automatically use its filename as name for the attachment. + + + validator.part_lot.owner_must_match_storage_location_owner + The owner of this lot must match the owner of the selected storage location (%owner_name%)! + +