Compare commits

..

60 Commits

Author SHA1 Message Date
Jan Böhmer
9c4e9066f9 Bump to version 1.4.0 2023-05-27 19:29:47 +02:00
Jan Böhmer
b4d1af2bce Merge remote-tracking branch 'origin/l10n_master' 2023-05-27 19:29:28 +02:00
Jan Böhmer
5ec676c40c Fixed static analysis issue 2023-05-27 19:29:00 +02:00
Jan Böhmer
5096aea5bb New translations security.en.xlf (English) 2023-05-27 19:26:51 +02:00
Jan Böhmer
feedd190dc New translations validators.en.xlf (English) 2023-05-27 19:26:51 +02:00
Jan Böhmer
3423fffaca New translations messages.en.xlf (English) 2023-05-27 19:26:50 +02:00
Jan Böhmer
1624fd2e28 New translations security.en.xlf (German) 2023-05-27 19:26:42 +02:00
Jan Böhmer
10b3094d5e New translations validators.en.xlf (German) 2023-05-27 19:26:42 +02:00
Jan Böhmer
580e638f67 New translations messages.en.xlf (German) 2023-05-27 19:26:41 +02:00
Jan Böhmer
e44428f87c Updated dependencies. 2023-05-27 19:24:14 +02:00
Jan Böhmer
379f7ef865 Implemented proper voters for attachments and parameters, so we can decide access for log details 2023-05-27 19:17:27 +02:00
Jan Böhmer
427f6e4d55 Merge remote-tracking branch 'origin/l10n_master' 2023-05-23 23:12:56 +02:00
Jan Böhmer
07a1e9fc3c New translations messages.en.xlf (English) 2023-05-23 23:09:42 +02:00
Jan Böhmer
78d64e8f1b New translations messages.en.xlf (German) 2023-05-23 23:09:32 +02:00
Jan Böhmer
559a9a9f3e New translations messages.en.xlf (German) 2023-05-23 22:45:26 +02:00
Jan Böhmer
ac6dd23fd6 Respect different currencies for pricedetails when importing from PartKeepr 2023-05-22 23:34:58 +02:00
Jan Böhmer
1e515df0b5 Fixed previous commit: Use the same behavior to determine the extension of file attachments like PartKeepr does, to ensure that all attachments are shown as available
This fixes issue #291
2023-05-22 23:06:41 +02:00
Jan Böhmer
35490762a6 Use the same behavior to determine the extension of file attachments like PartKeepr does, to ensure that all attachments are shown as available
This fixes issue #291
2023-05-22 22:55:18 +02:00
Jan Böhmer
c25e23d3d9 New translations messages.en.xlf (English) 2023-05-18 23:36:43 +02:00
Jan Böhmer
8bb8257e62 Added a log entry detail page for collection element deleted log entries. 2023-05-18 23:05:40 +02:00
Jan Böhmer
5f096927bd New translations messages.en.xlf (English) 2023-05-16 00:17:44 +02:00
Jan Böhmer
434826c125 Use default CodeQL workflow which is configured via repo settings and not via a action file 2023-05-16 00:16:50 +02:00
Jan Böhmer
89595cd5dc We are in development of version 1.4.0 now 2023-05-16 00:08:57 +02:00
Jan Böhmer
d991e15a94 Merge branch 'log_detail_page' 2023-05-16 00:08:12 +02:00
Jan Böhmer
6a1aefa5a5 Allow access to log detail page (only) if a user has permission to show_history of an entity 2023-05-16 00:05:54 +02:00
Jan Böhmer
272684e7eb Visualize generic object/JSON data of element history data as pretty tree structure on log detail page 2023-05-15 23:55:36 +02:00
Jan Böhmer
9be3eba694 Added button to delete a log entry via the log detail page. 2023-05-15 23:02:30 +02:00
Jan Böhmer
5a3fc0fb43 Show and link which log entry was undone/reverted on log detail page 2023-05-15 22:42:08 +02:00
Jan Böhmer
47ef8e9568 Updated dependencies 2023-05-15 00:36:36 +02:00
Jan Böhmer
e4285bbc78 delete_btn_controller: Keep the value and name of the original clicked button
This fixes an error message when undoing or reverting a log entry
2023-05-15 00:34:06 +02:00
Jan Böhmer
49b6a42791 Added buttons for revert and undo to the log detail page 2023-05-15 00:16:49 +02:00
Jan Böhmer
b62fd602f2 Show the diff of element edited log entries on detail pages 2023-05-14 23:08:14 +02:00
Jan Böhmer
923e40ed8f Add the data after the change to a element edited log entry, so you can easily view the changes in log detail pages 2023-05-14 21:41:00 +02:00
Jan Böhmer
3c724a227a Merge branch 'master' into log_detail_page 2023-05-14 16:43:52 +02:00
Jan Böhmer
90d26eb16a New translations messages.en.xlf (English) 2023-05-09 01:18:42 +02:00
Jan Böhmer
b629744e1a We are in development of v1.3.4 now 2023-05-09 00:27:18 +02:00
Jan Böhmer
b0ab43c39a Show a proper error message table when encountering an invalid regex statement on SQLite
This is related to #289
2023-05-09 00:26:40 +02:00
Jan Böhmer
2c33b381c1 Allow to unselect name, category, description fields etc in search functionm
Before this commit it was ignored, if the checkboxes for these fields were unchecked.
2023-05-08 23:53:59 +02:00
Jan Böhmer
c50a80e8df Show an error message in table instead of a 500 error when MySQL encounters an invalid Regex expression
This fixes issue #289
2023-05-08 23:42:25 +02:00
Jan Böhmer
bafbd63610 Bumped version to 1.3.3 2023-05-08 00:10:21 +02:00
Jan Böhmer
557347d42d Fixed legacy database migration for newer MySQL version 2023-05-07 21:43:01 +02:00
Jan Böhmer
c828aa3bc0 Use correct table name for test_legacy_import workflow 2023-05-07 02:58:59 +02:00
Jan Böhmer
376d29e829 Use correct table name for test_legacy_import workflow 2023-05-07 02:52:31 +02:00
Jan Böhmer
97ee4f36e3 Fixed typo in legacy import test script 2023-05-07 02:44:07 +02:00
Jan Böhmer
d766f255ef Let job fail, when an error occurs during the legacy test script 2023-05-07 02:38:54 +02:00
Jan Böhmer
19ae9e7456 Fixed issue preventing migration from legacy Part-DB version
This bug was introduced during the fix for issue #260 (commit 2717d7d). With this commit issue #287 should be fixed
2023-05-07 02:29:32 +02:00
Jan Böhmer
61ea312403 Added continous testing for legacy Part-DB import 2023-05-07 02:27:16 +02:00
Jan Böhmer
13193c9368 Fixed issue that searching in part notes were not possible
This fixes issue #288
2023-05-07 01:05:12 +02:00
Jan Böhmer
31e0ce4c64 Updated various yarn dependencies.
We can not upgrade marked to 5.0.0 yet, as it requires node18. We will require this version when we upgrade to PHP 8.1
2023-05-07 00:48:47 +02:00
Jan Böhmer
43238aff0a Updated CKEDITOR to 37.1.0 2023-05-07 00:11:13 +02:00
Jan Böhmer
d598bfc35b Updated dependencies. 2023-05-06 22:25:56 +02:00
Jan Böhmer
1c836918ca Fixed various Partkeepr import issues
On Linux mysql table names can contain uppercase characters, and we expect always lowercase character, so we now normalize the tablenames to lowercase.

Also fixed some type errors on part parameters and improved performace for orderdetails import.

This fixes issue #286
2023-05-02 00:40:00 +02:00
Jan Böhmer
4a6a3b9269 Moved doctrine/data-fixtures to prod requirement
We use it for database purging in partkeepr import command. This fixes issue #285
2023-05-01 23:09:59 +02:00
Jan Böhmer
1534f780aa Show a table with the old data in log entry details page 2023-05-01 01:38:14 +02:00
Jan Böhmer
4c6ceab8e8 Merge branch 'master' into log_detail_page 2023-04-29 22:46:38 +02:00
Jan Böhmer
f3fc01b740 New translations security.en.xlf (English) 2023-04-11 13:48:44 +02:00
Jan Böhmer
a201be5a01 New translations validators.en.xlf (English) 2023-04-11 13:48:43 +02:00
Jan Böhmer
ebf2035351 New translations messages.en.xlf (English) 2023-04-11 13:48:42 +02:00
Jan Böhmer
69fc28d5d6 Added better formatted extra section for certain log types 2023-04-10 23:13:09 +02:00
Jan Böhmer
4107535b19 Added basic log entry info page 2023-04-10 00:30:23 +02:00
63 changed files with 6344 additions and 2743 deletions

3
.env
View File

@@ -71,6 +71,9 @@ HISTORY_SAVE_CHANGED_FIELDS=1
HISTORY_SAVE_CHANGED_DATA=1
# Save the data of an element that gets removed into log entry. This allows to undelete an element
HISTORY_SAVE_REMOVED_DATA=1
# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page
# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1
HISTORY_SAVE_NEW_DATA=1
###################################################################################
# Error pages settings

View File

@@ -0,0 +1,752 @@
-- phpMyAdmin SQL Dump
-- version 5.1.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 07. Mai 2023 um 01:58
-- Server-Version: 10.6.5-MariaDB-log
-- PHP-Version: 8.1.2
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `partdb_demo`
--
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachements`
--
CREATE TABLE `attachements` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`class_name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
`element_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`show_in_table` tinyint(1) NOT NULL DEFAULT 0,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachements`
--
INSERT INTO `attachements` (`id`, `name`, `class_name`, `element_id`, `type_id`, `filename`, `show_in_table`, `last_modified`) VALUES
(1, 'BC547', 'Part', 2, 2, '%BASE%/data/media/bc547.pdf', 1, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachement_types`
--
CREATE TABLE `attachement_types` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachement_types`
--
INSERT INTO `attachement_types` (`id`, `name`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Bilder', NULL, NULL, '2017-10-21 17:58:48', '0000-00-00 00:00:00'),
(2, 'Datenblätter', NULL, NULL, '2017-10-21 17:58:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `categories`
--
CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`disable_footprints` tinyint(1) NOT NULL DEFAULT 0,
`disable_manufacturers` tinyint(1) NOT NULL DEFAULT 0,
`disable_autodatasheets` tinyint(1) NOT NULL DEFAULT 0,
`disable_properties` tinyint(1) NOT NULL DEFAULT 0,
`partname_regex` text COLLATE utf8mb3_unicode_ci NOT NULL,
`partname_hint` text COLLATE utf8mb3_unicode_ci NOT NULL,
`default_description` text COLLATE utf8mb3_unicode_ci NOT NULL,
`default_comment` text COLLATE utf8mb3_unicode_ci NOT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `categories`
--
INSERT INTO `categories` (`id`, `name`, `parent_id`, `disable_footprints`, `disable_manufacturers`, `disable_autodatasheets`, `disable_properties`, `partname_regex`, `partname_hint`, `default_description`, `default_comment`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'aktive Bauteile', NULL, 0, 0, 0, 0, '', '', '', '', NULL, '2017-10-21 17:58:49', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `devices`
--
CREATE TABLE `devices` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 0,
`order_only_missing_parts` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `devices`
--
INSERT INTO `devices` (`id`, `name`, `parent_id`, `order_quantity`, `order_only_missing_parts`, `datetime_added`, `last_modified`, `comment`) VALUES
(1, 'Test', NULL, 0, 0, '2015-04-16 15:08:56', '0000-00-00 00:00:00', NULL);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `device_parts`
--
CREATE TABLE `device_parts` (
`id` int(11) NOT NULL,
`id_part` int(11) NOT NULL DEFAULT 0,
`id_device` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 0,
`mountnames` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `device_parts`
--
INSERT INTO `device_parts` (`id`, `id_part`, `id_device`, `quantity`, `mountnames`) VALUES
(1, 2, 1, 1, '');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `footprints`
--
CREATE TABLE `footprints` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename_3d` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `footprints`
--
INSERT INTO `footprints` (`id`, `name`, `filename`, `filename_3d`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'LEDs', '%BASE%/img/footprints/Optik/LEDs/Bedrahtet/LED-GELB_3MM.png', '', NULL, NULL, '2017-10-21 17:58:49', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `groups`
--
CREATE TABLE `groups` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` mediumtext DEFAULT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
--
-- Daten für Tabelle `groups`
--
INSERT INTO `groups` (`id`, `name`, `parent_id`, `comment`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'admins', NULL, 'Users of this group can do everything: Read, Write and Administrative actions.', 21, 1365, 87381, 85, 85, 21, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2017-10-21 17:58:46', '2018-10-08 17:27:41'),
(2, 'readonly', NULL, 'Users of this group can only read informations, use tools, and don\'t have access to administrative tools.', 2, 2730, 43690, 25, 170, 42, 2778027689, 9, 9, 9, 9, 9, 9, 9, 9, 9, 649, 649, 649, 1705, 649, 1705, 1705, 1705, 1705, 1705, 681, 1366, 165, '2017-10-21 17:58:46', '2018-10-08 17:28:35'),
(3, 'users', NULL, 'Users of this group, can edit part informations, create new ones, etc. but are not allowed to use administrative tools. (But can read current configuration, and see Server status)', 42, 2730, 43689, 89, 105, 41, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2017-10-21 17:58:46', '2018-10-08 17:28:17');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `internal`
--
CREATE TABLE `internal` (
`keyName` char(30) CHARACTER SET ascii NOT NULL,
`keyValue` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `internal`
--
INSERT INTO `internal` (`keyName`, `keyValue`) VALUES
('dbVersion', '26');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `log`
--
CREATE TABLE `log` (
`id` int(11) NOT NULL,
`datetime` timestamp NOT NULL DEFAULT current_timestamp(),
`id_user` int(11) NOT NULL,
`level` tinyint(4) NOT NULL,
`type` smallint(6) NOT NULL,
`target_id` int(11) NOT NULL,
`target_type` smallint(6) NOT NULL,
`extra` mediumtext NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `manufacturers`
--
CREATE TABLE `manufacturers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `manufacturers`
--
INSERT INTO `manufacturers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Atmel', NULL, '', '', '', '', '', '', '2015-03-01 11:27:10', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `orderdetails`
--
CREATE TABLE `orderdetails` (
`id` int(11) NOT NULL,
`part_id` int(11) NOT NULL,
`id_supplier` int(11) NOT NULL DEFAULT 0,
`supplierpartnr` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`obsolete` tinyint(1) DEFAULT 0,
`supplier_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `parts`
--
CREATE TABLE `parts` (
`id` int(11) NOT NULL,
`id_category` int(11) NOT NULL DEFAULT 0,
`name` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`description` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`instock` int(11) NOT NULL DEFAULT 0,
`mininstock` int(11) NOT NULL DEFAULT 0,
`comment` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`visible` tinyint(1) NOT NULL,
`id_footprint` int(11) DEFAULT NULL,
`id_storelocation` int(11) DEFAULT NULL,
`order_orderdetails_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 1,
`manual_order` tinyint(1) NOT NULL DEFAULT 0,
`id_manufacturer` int(11) DEFAULT NULL,
`id_master_picture_attachement` int(11) DEFAULT NULL,
`manufacturer_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`favorite` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `parts`
--
INSERT INTO `parts` (`id`, `id_category`, `name`, `description`, `instock`, `mininstock`, `comment`, `visible`, `id_footprint`, `id_storelocation`, `order_orderdetails_id`, `order_quantity`, `manual_order`, `id_manufacturer`, `id_master_picture_attachement`, `manufacturer_product_url`, `datetime_added`, `last_modified`, `favorite`) VALUES
(2, 1, 'BC547C', 'NPN 45V 0,1A 0,5W', 59, 0, '', 0, 1, 1, NULL, 1, 0, NULL, NULL, '', '2015-03-01 10:40:31', '2016-12-26 10:48:49', 0);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `pricedetails`
--
CREATE TABLE `pricedetails` (
`id` int(11) NOT NULL,
`orderdetails_id` int(11) NOT NULL,
`price` decimal(11,5) DEFAULT NULL,
`price_related_quantity` int(11) NOT NULL DEFAULT 1,
`min_discount_quantity` int(11) NOT NULL DEFAULT 1,
`manual_input` tinyint(1) NOT NULL DEFAULT 1,
`last_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `storelocations`
--
CREATE TABLE `storelocations` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`is_full` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `storelocations`
--
INSERT INTO `storelocations` (`id`, `name`, `parent_id`, `is_full`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Halbleiter I', NULL, 0, '2015-03-01 11:26:37', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `suppliers`
--
CREATE TABLE `suppliers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `suppliers`
--
INSERT INTO `suppliers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Test', NULL, '', '', '', '', '', 'Test', '2015-03-01 10:37:23', NULL, '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`first_name` tinytext DEFAULT NULL,
`last_name` tinytext DEFAULT NULL,
`department` tinytext DEFAULT NULL,
`email` tinytext DEFAULT NULL,
`need_pw_change` tinyint(1) NOT NULL DEFAULT 0,
`group_id` int(11) DEFAULT NULL,
`config_language` tinytext DEFAULT NULL,
`config_timezone` tinytext DEFAULT NULL,
`config_theme` tinytext DEFAULT NULL,
`config_currency` tinytext DEFAULT NULL,
`config_image_path` text NOT NULL,
`config_instock_comment_w` text NOT NULL,
`config_instock_comment_a` text NOT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
--
-- Daten für Tabelle `users`
--
INSERT INTO `users` (`id`, `name`, `password`, `first_name`, `last_name`, `department`, `email`, `need_pw_change`, `group_id`, `config_language`, `config_timezone`, `config_theme`, `config_currency`, `config_image_path`, `config_instock_comment_w`, `config_instock_comment_a`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'anonymous', '', '', '', '', '', 0, 2, '', '', '', NULL, '', '', '', 21848, 20480, 0, 0, 0, 0, 0, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21520, 21520, 21520, 20480, 21520, 20480, 20480, 20480, 20480, 20480, 21504, 20480, 0, '2017-10-21 17:58:46', '2018-02-18 12:46:58'),
(2, 'admin', '$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe', 'Admin', 'Ad', NULL, 'admin@ras.pi', 0, 1, '', '', '', NULL, '', '', '', 21845, 21845, 21845, 21, 85, 21, 349525, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 0, '2017-10-21 17:58:46', '2017-12-23 11:04:48');
--
-- Indizes der exportierten Tabellen
--
--
-- Indizes für die Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD PRIMARY KEY (`id`),
ADD KEY `attachements_class_name_k` (`class_name`),
ADD KEY `attachements_element_id_k` (`element_id`),
ADD KEY `attachements_type_id_fk` (`type_id`);
--
-- Indizes für die Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD PRIMARY KEY (`id`),
ADD KEY `attachement_types_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `categories`
--
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`),
ADD KEY `categories_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `devices`
--
ALTER TABLE `devices`
ADD PRIMARY KEY (`id`),
ADD KEY `devices_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `device_parts`
--
ALTER TABLE `device_parts`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `device_parts_combination_uk` (`id_part`,`id_device`),
ADD KEY `device_parts_id_part_k` (`id_part`),
ADD KEY `device_parts_id_device_k` (`id_device`);
--
-- Indizes für die Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD PRIMARY KEY (`id`),
ADD KEY `footprints_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `groups`
--
ALTER TABLE `groups`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- Indizes für die Tabelle `internal`
--
ALTER TABLE `internal`
ADD UNIQUE KEY `keyName` (`keyName`);
--
-- Indizes für die Tabelle `log`
--
ALTER TABLE `log`
ADD PRIMARY KEY (`id`),
ADD KEY `id_user` (`id_user`);
--
-- Indizes für die Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD PRIMARY KEY (`id`),
ADD KEY `manufacturers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
ADD PRIMARY KEY (`id`),
ADD KEY `orderdetails_part_id_k` (`part_id`),
ADD KEY `orderdetails_id_supplier_k` (`id_supplier`);
--
-- Indizes für die Tabelle `parts`
--
ALTER TABLE `parts`
ADD PRIMARY KEY (`id`),
ADD KEY `parts_id_category_k` (`id_category`),
ADD KEY `parts_id_footprint_k` (`id_footprint`),
ADD KEY `parts_id_storelocation_k` (`id_storelocation`),
ADD KEY `parts_order_orderdetails_id_k` (`order_orderdetails_id`),
ADD KEY `parts_id_manufacturer_k` (`id_manufacturer`),
ADD KEY `favorite` (`favorite`);
--
-- Indizes für die Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pricedetails_combination_uk` (`orderdetails_id`,`min_discount_quantity`),
ADD KEY `pricedetails_orderdetails_id_k` (`orderdetails_id`);
--
-- Indizes für die Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD PRIMARY KEY (`id`),
ADD KEY `storelocations_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD PRIMARY KEY (`id`),
ADD KEY `suppliers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- AUTO_INCREMENT für exportierte Tabellen
--
--
-- AUTO_INCREMENT für Tabelle `attachements`
--
ALTER TABLE `attachements`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=201;
--
-- AUTO_INCREMENT für Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `categories`
--
ALTER TABLE `categories`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=123;
--
-- AUTO_INCREMENT für Tabelle `devices`
--
ALTER TABLE `devices`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `device_parts`
--
ALTER TABLE `device_parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;
--
-- AUTO_INCREMENT für Tabelle `footprints`
--
ALTER TABLE `footprints`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=82;
--
-- AUTO_INCREMENT für Tabelle `groups`
--
ALTER TABLE `groups`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- AUTO_INCREMENT für Tabelle `log`
--
ALTER TABLE `log`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=218;
--
-- AUTO_INCREMENT für Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=650;
--
-- AUTO_INCREMENT für Tabelle `parts`
--
ALTER TABLE `parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1171;
--
-- AUTO_INCREMENT für Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=437;
--
-- AUTO_INCREMENT für Tabelle `storelocations`
--
ALTER TABLE `storelocations`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=340;
--
-- AUTO_INCREMENT für Tabelle `suppliers`
--
ALTER TABLE `suppliers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- Constraints der exportierten Tabellen
--
--
-- Constraints der Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD CONSTRAINT `attachements_type_id_fk` FOREIGN KEY (`type_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD CONSTRAINT `attachement_types_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `categories`
--
ALTER TABLE `categories`
ADD CONSTRAINT `categories_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`);
--
-- Constraints der Tabelle `devices`
--
ALTER TABLE `devices`
ADD CONSTRAINT `devices_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `devices` (`id`);
--
-- Constraints der Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD CONSTRAINT `footprints_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `footprints` (`id`);
--
-- Constraints der Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD CONSTRAINT `manufacturers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `manufacturers` (`id`);
--
-- Constraints der Tabelle `parts`
--
ALTER TABLE `parts`
ADD CONSTRAINT `parts_id_footprint_fk` FOREIGN KEY (`id_footprint`) REFERENCES `footprints` (`id`),
ADD CONSTRAINT `parts_id_manufacturer_fk` FOREIGN KEY (`id_manufacturer`) REFERENCES `manufacturers` (`id`),
ADD CONSTRAINT `parts_id_storelocation_fk` FOREIGN KEY (`id_storelocation`) REFERENCES `storelocations` (`id`),
ADD CONSTRAINT `parts_order_orderdetails_id_fk` FOREIGN KEY (`order_orderdetails_id`) REFERENCES `orderdetails` (`id`);
--
-- Constraints der Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD CONSTRAINT `storelocations_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `storelocations` (`id`);
--
-- Constraints der Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD CONSTRAINT `suppliers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `suppliers` (`id`);
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -0,0 +1,736 @@
-- phpMyAdmin SQL Dump
-- version 5.1.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 07. Mai 2023 um 01:48
-- Server-Version: 10.6.5-MariaDB-log
-- PHP-Version: 8.1.2
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `partdb-legacy`
--
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachements`
--
CREATE TABLE `attachements` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`class_name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
`element_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`show_in_table` tinyint(1) NOT NULL DEFAULT 0,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `attachement_types`
--
CREATE TABLE `attachement_types` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `attachement_types`
--
INSERT INTO `attachement_types` (`id`, `name`, `parent_id`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Bilder', NULL, NULL, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'Datenblätter', NULL, NULL, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `categories`
--
CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`disable_footprints` tinyint(1) NOT NULL DEFAULT 0,
`disable_manufacturers` tinyint(1) NOT NULL DEFAULT 0,
`disable_autodatasheets` tinyint(1) NOT NULL DEFAULT 0,
`disable_properties` tinyint(1) NOT NULL DEFAULT 0,
`partname_regex` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`partname_hint` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`default_description` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`default_comment` text COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT '',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `categories`
--
INSERT INTO `categories` (`id`, `name`, `parent_id`, `disable_footprints`, `disable_manufacturers`, `disable_autodatasheets`, `disable_properties`, `partname_regex`, `partname_hint`, `default_description`, `default_comment`, `comment`, `datetime_added`, `last_modified`) VALUES
(1, 'Test', NULL, 0, 0, 0, 0, '', '', '', '', '', '2023-01-07 18:32:29', '2023-01-07 18:32:29');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `devices`
--
CREATE TABLE `devices` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 0,
`order_only_missing_parts` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `device_parts`
--
CREATE TABLE `device_parts` (
`id` int(11) NOT NULL,
`id_part` int(11) NOT NULL DEFAULT 0,
`id_device` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 0,
`mountnames` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `footprints`
--
CREATE TABLE `footprints` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`filename_3d` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `groups`
--
CREATE TABLE `groups` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`comment` mediumtext DEFAULT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `groups`
--
INSERT INTO `groups` (`id`, `name`, `parent_id`, `comment`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'admins', NULL, 'Users of this group can do everything: Read, Write and Administrative actions.', 21, 1365, 87381, 85, 85, 21, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'readonly', NULL, 'Users of this group can only read informations, use tools, and don\'t have access to administrative tools.', 42, 2730, 174762, 154, 170, 42, -1516939607, 9, 9, 9, 9, 9, 9, 9, 9, 9, 649, 649, 649, 1705, 649, 1705, 1705, 1705, 1705, 1705, 681, 1366, 165, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(3, 'users', NULL, 'Users of this group, can edit part informations, create new ones, etc. but are not allowed to use administrative tools. (But can read current configuration, and see Server status)', 42, 2730, 109226, 89, 105, 41, 1431655765, 5, 5, 5, 5, 5, 5, 5, 5, 5, 325, 325, 325, 5461, 325, 5461, 5461, 5461, 5461, 5461, 1365, 1365, 85, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `internal`
--
CREATE TABLE `internal` (
`keyName` char(30) CHARACTER SET ascii NOT NULL,
`keyValue` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `internal`
--
INSERT INTO `internal` (`keyName`, `keyValue`) VALUES
('dbVersion', '26');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `log`
--
CREATE TABLE `log` (
`id` int(11) NOT NULL,
`datetime` timestamp NOT NULL DEFAULT current_timestamp(),
`id_user` int(11) NOT NULL,
`level` tinyint(4) NOT NULL,
`type` smallint(6) NOT NULL,
`target_id` int(11) NOT NULL,
`target_type` smallint(6) NOT NULL,
`extra` mediumtext NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `log`
--
INSERT INTO `log` (`id`, `datetime`, `id_user`, `level`, `type`, `target_id`, `target_type`, `extra`) VALUES
(1, '2023-01-07 18:31:48', 1, 4, 10, 0, 0, '{\"o\":0,\"n\":26,\"s\":true}'),
(2, '2023-01-07 18:32:13', 2, 6, 1, 2, 1, '{\"i\":\"::\"}'),
(3, '2023-01-07 18:32:29', 2, 6, 6, 1, 4, '[]'),
(4, '2023-01-07 18:32:53', 2, 6, 6, 1, 12, '[]'),
(5, '2023-01-07 18:33:26', 2, 6, 6, 1, 10, '{\"i\":0}');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `manufacturers`
--
CREATE TABLE `manufacturers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `orderdetails`
--
CREATE TABLE `orderdetails` (
`id` int(11) NOT NULL,
`part_id` int(11) NOT NULL,
`id_supplier` int(11) NOT NULL DEFAULT 0,
`supplierpartnr` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`obsolete` tinyint(1) DEFAULT 0,
`supplier_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `orderdetails`
--
INSERT INTO `orderdetails` (`id`, `part_id`, `id_supplier`, `supplierpartnr`, `obsolete`, `supplier_product_url`, `datetime_added`) VALUES
(1, 1, 1, 'BC547', 0, '', '2023-01-07 18:45:59'),
(2, 1, 1, 'Test', 0, '', '2023-01-07 18:46:09');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `parts`
--
CREATE TABLE `parts` (
`id` int(11) NOT NULL,
`id_category` int(11) NOT NULL DEFAULT 0,
`name` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`description` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`instock` int(11) NOT NULL DEFAULT 0,
`mininstock` int(11) NOT NULL DEFAULT 0,
`comment` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`visible` tinyint(1) NOT NULL,
`id_footprint` int(11) DEFAULT NULL,
`id_storelocation` int(11) DEFAULT NULL,
`order_orderdetails_id` int(11) DEFAULT NULL,
`order_quantity` int(11) NOT NULL DEFAULT 1,
`manual_order` tinyint(1) NOT NULL DEFAULT 0,
`id_manufacturer` int(11) DEFAULT NULL,
`id_master_picture_attachement` int(11) DEFAULT NULL,
`manufacturer_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`favorite` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `parts`
--
INSERT INTO `parts` (`id`, `id_category`, `name`, `description`, `instock`, `mininstock`, `comment`, `visible`, `id_footprint`, `id_storelocation`, `order_orderdetails_id`, `order_quantity`, `manual_order`, `id_manufacturer`, `id_master_picture_attachement`, `manufacturer_product_url`, `datetime_added`, `last_modified`, `favorite`) VALUES
(1, 1, 'BC547', '', 0, 0, '', 0, NULL, NULL, NULL, 1, 0, NULL, NULL, '', '2023-01-07 18:33:26', '2023-01-07 18:33:26', 0);
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `pricedetails`
--
CREATE TABLE `pricedetails` (
`id` int(11) NOT NULL,
`orderdetails_id` int(11) NOT NULL,
`price` decimal(11,5) DEFAULT NULL,
`price_related_quantity` int(11) NOT NULL DEFAULT 1,
`min_discount_quantity` int(11) NOT NULL DEFAULT 1,
`manual_input` tinyint(1) NOT NULL DEFAULT 1,
`last_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `pricedetails`
--
INSERT INTO `pricedetails` (`id`, `orderdetails_id`, `price`, `price_related_quantity`, `min_discount_quantity`, `manual_input`, `last_modified`) VALUES
(1, 2, '3.55000', 1, 1, 1, '2023-01-07 18:46:19');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `storelocations`
--
CREATE TABLE `storelocations` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`is_full` tinyint(1) NOT NULL DEFAULT 0,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `suppliers`
--
CREATE TABLE `suppliers` (
`id` int(11) NOT NULL,
`name` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`address` mediumtext COLLATE utf8mb3_unicode_ci NOT NULL,
`phone_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`fax_number` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`email_address` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`website` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`auto_product_url` tinytext COLLATE utf8mb3_unicode_ci NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`comment` text COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
--
-- Daten für Tabelle `suppliers`
--
INSERT INTO `suppliers` (`id`, `name`, `parent_id`, `address`, `phone_number`, `fax_number`, `email_address`, `website`, `auto_product_url`, `datetime_added`, `comment`, `last_modified`) VALUES
(1, 'Reichelt', NULL, '', '', '', '', '', '', '2023-01-07 18:32:53', '', '2023-01-07 18:32:53');
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`first_name` tinytext DEFAULT NULL,
`last_name` tinytext DEFAULT NULL,
`department` tinytext DEFAULT NULL,
`email` tinytext DEFAULT NULL,
`need_pw_change` tinyint(1) NOT NULL DEFAULT 0,
`group_id` int(11) DEFAULT NULL,
`config_language` tinytext DEFAULT NULL,
`config_timezone` tinytext DEFAULT NULL,
`config_theme` tinytext DEFAULT NULL,
`config_currency` tinytext DEFAULT NULL,
`config_image_path` text NOT NULL,
`config_instock_comment_w` text NOT NULL,
`config_instock_comment_a` text NOT NULL,
`perms_system` int(11) NOT NULL,
`perms_groups` int(11) NOT NULL,
`perms_users` int(11) NOT NULL,
`perms_self` int(11) NOT NULL,
`perms_system_config` int(11) NOT NULL,
`perms_system_database` int(11) NOT NULL,
`perms_parts` bigint(11) NOT NULL,
`perms_parts_name` smallint(6) NOT NULL,
`perms_parts_description` smallint(6) NOT NULL,
`perms_parts_instock` smallint(6) NOT NULL,
`perms_parts_mininstock` smallint(6) NOT NULL,
`perms_parts_footprint` smallint(6) NOT NULL,
`perms_parts_storelocation` smallint(6) NOT NULL,
`perms_parts_manufacturer` smallint(6) NOT NULL,
`perms_parts_comment` smallint(6) NOT NULL,
`perms_parts_order` smallint(6) NOT NULL,
`perms_parts_orderdetails` smallint(6) NOT NULL,
`perms_parts_prices` smallint(6) NOT NULL,
`perms_parts_attachements` smallint(6) NOT NULL,
`perms_devices` int(11) NOT NULL,
`perms_devices_parts` int(11) NOT NULL,
`perms_storelocations` int(11) NOT NULL,
`perms_footprints` int(11) NOT NULL,
`perms_categories` int(11) NOT NULL,
`perms_suppliers` int(11) NOT NULL,
`perms_manufacturers` int(11) NOT NULL,
`perms_attachement_types` int(11) NOT NULL,
`perms_tools` int(11) NOT NULL,
`perms_labels` smallint(6) NOT NULL,
`datetime_added` timestamp NOT NULL DEFAULT current_timestamp(),
`last_modified` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Daten für Tabelle `users`
--
INSERT INTO `users` (`id`, `name`, `password`, `first_name`, `last_name`, `department`, `email`, `need_pw_change`, `group_id`, `config_language`, `config_timezone`, `config_theme`, `config_currency`, `config_image_path`, `config_instock_comment_w`, `config_instock_comment_a`, `perms_system`, `perms_groups`, `perms_users`, `perms_self`, `perms_system_config`, `perms_system_database`, `perms_parts`, `perms_parts_name`, `perms_parts_description`, `perms_parts_instock`, `perms_parts_mininstock`, `perms_parts_footprint`, `perms_parts_storelocation`, `perms_parts_manufacturer`, `perms_parts_comment`, `perms_parts_order`, `perms_parts_orderdetails`, `perms_parts_prices`, `perms_parts_attachements`, `perms_devices`, `perms_devices_parts`, `perms_storelocations`, `perms_footprints`, `perms_categories`, `perms_suppliers`, `perms_manufacturers`, `perms_attachement_types`, `perms_tools`, `perms_labels`, `datetime_added`, `last_modified`) VALUES
(1, 'anonymous', '', '', '', '', '', 0, 2, NULL, NULL, NULL, NULL, '', '', '', 21844, 20480, 0, 0, 0, 0, 0, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21840, 21520, 21520, 21520, 20480, 21520, 20480, 20480, 20480, 20480, 20480, 21504, 20480, 0, '2023-01-07 18:31:48', '0000-00-00 00:00:00'),
(2, 'admin', '$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe$2a$12$j0RKrKlx60bzX1DWMyXwjeaW.pe3bFjAK8ByIGnvjrRnET2JtsFoe', '', '', '', '', 1, 1, NULL, NULL, NULL, NULL, '', '', '', 21845, 21845, 21845, 21, 85, 21, 349525, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 0, '2023-01-07 18:31:48', '0000-00-00 00:00:00');
--
-- Indizes der exportierten Tabellen
--
--
-- Indizes für die Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD PRIMARY KEY (`id`),
ADD KEY `attachements_class_name_k` (`class_name`),
ADD KEY `attachements_element_id_k` (`element_id`),
ADD KEY `attachements_type_id_fk` (`type_id`);
--
-- Indizes für die Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD PRIMARY KEY (`id`),
ADD KEY `attachement_types_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `categories`
--
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`),
ADD KEY `categories_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `devices`
--
ALTER TABLE `devices`
ADD PRIMARY KEY (`id`),
ADD KEY `devices_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `device_parts`
--
ALTER TABLE `device_parts`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `device_parts_combination_uk` (`id_part`,`id_device`),
ADD KEY `device_parts_id_part_k` (`id_part`),
ADD KEY `device_parts_id_device_k` (`id_device`);
--
-- Indizes für die Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD PRIMARY KEY (`id`),
ADD KEY `footprints_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `groups`
--
ALTER TABLE `groups`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- Indizes für die Tabelle `internal`
--
ALTER TABLE `internal`
ADD UNIQUE KEY `keyName` (`keyName`);
--
-- Indizes für die Tabelle `log`
--
ALTER TABLE `log`
ADD PRIMARY KEY (`id`),
ADD KEY `id_user` (`id_user`);
--
-- Indizes für die Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD PRIMARY KEY (`id`),
ADD KEY `manufacturers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
ADD PRIMARY KEY (`id`),
ADD KEY `orderdetails_part_id_k` (`part_id`),
ADD KEY `orderdetails_id_supplier_k` (`id_supplier`);
--
-- Indizes für die Tabelle `parts`
--
ALTER TABLE `parts`
ADD PRIMARY KEY (`id`),
ADD KEY `parts_id_category_k` (`id_category`),
ADD KEY `parts_id_footprint_k` (`id_footprint`),
ADD KEY `parts_id_storelocation_k` (`id_storelocation`),
ADD KEY `parts_order_orderdetails_id_k` (`order_orderdetails_id`),
ADD KEY `parts_id_manufacturer_k` (`id_manufacturer`),
ADD KEY `favorite` (`favorite`);
--
-- Indizes für die Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pricedetails_combination_uk` (`orderdetails_id`,`min_discount_quantity`),
ADD KEY `pricedetails_orderdetails_id_k` (`orderdetails_id`);
--
-- Indizes für die Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD PRIMARY KEY (`id`),
ADD KEY `storelocations_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD PRIMARY KEY (`id`),
ADD KEY `suppliers_parent_id_k` (`parent_id`);
--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- AUTO_INCREMENT für exportierte Tabellen
--
--
-- AUTO_INCREMENT für Tabelle `attachements`
--
ALTER TABLE `attachements`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `categories`
--
ALTER TABLE `categories`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `devices`
--
ALTER TABLE `devices`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `device_parts`
--
ALTER TABLE `device_parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `footprints`
--
ALTER TABLE `footprints`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `groups`
--
ALTER TABLE `groups`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- AUTO_INCREMENT für Tabelle `log`
--
ALTER TABLE `log`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT für Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `orderdetails`
--
ALTER TABLE `orderdetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT für Tabelle `parts`
--
ALTER TABLE `parts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `pricedetails`
--
ALTER TABLE `pricedetails`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `storelocations`
--
ALTER TABLE `storelocations`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT für Tabelle `suppliers`
--
ALTER TABLE `suppliers`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- Constraints der exportierten Tabellen
--
--
-- Constraints der Tabelle `attachements`
--
ALTER TABLE `attachements`
ADD CONSTRAINT `attachements_type_id_fk` FOREIGN KEY (`type_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `attachement_types`
--
ALTER TABLE `attachement_types`
ADD CONSTRAINT `attachement_types_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `attachement_types` (`id`);
--
-- Constraints der Tabelle `categories`
--
ALTER TABLE `categories`
ADD CONSTRAINT `categories_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `categories` (`id`);
--
-- Constraints der Tabelle `devices`
--
ALTER TABLE `devices`
ADD CONSTRAINT `devices_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `devices` (`id`);
--
-- Constraints der Tabelle `footprints`
--
ALTER TABLE `footprints`
ADD CONSTRAINT `footprints_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `footprints` (`id`);
--
-- Constraints der Tabelle `manufacturers`
--
ALTER TABLE `manufacturers`
ADD CONSTRAINT `manufacturers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `manufacturers` (`id`);
--
-- Constraints der Tabelle `parts`
--
ALTER TABLE `parts`
ADD CONSTRAINT `parts_id_footprint_fk` FOREIGN KEY (`id_footprint`) REFERENCES `footprints` (`id`),
ADD CONSTRAINT `parts_id_manufacturer_fk` FOREIGN KEY (`id_manufacturer`) REFERENCES `manufacturers` (`id`),
ADD CONSTRAINT `parts_id_storelocation_fk` FOREIGN KEY (`id_storelocation`) REFERENCES `storelocations` (`id`),
ADD CONSTRAINT `parts_order_orderdetails_id_fk` FOREIGN KEY (`order_orderdetails_id`) REFERENCES `orderdetails` (`id`);
--
-- Constraints der Tabelle `storelocations`
--
ALTER TABLE `storelocations`
ADD CONSTRAINT `storelocations_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `storelocations` (`id`);
--
-- Constraints der Tabelle `suppliers`
--
ALTER TABLE `suppliers`
ADD CONSTRAINT `suppliers_parent_id_fk` FOREIGN KEY (`parent_id`) REFERENCES `suppliers` (`id`);
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -0,0 +1,54 @@
#
# This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
#
# Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
#!/bin/bash
# This script is used to test the legacy import of Part-DB
SQL_FILES_TO_TEST=("db_minimal.sql" "db_jbtronics.sql")
DB_NAME="legacy_db_test"
DB_USER="root"
DB_PASSWORD="root"
# Iterate over all given SQL files and import them into the mysql database with the given name, drop the database if it already exists before
for SQL_FILE in "${SQL_FILES_TO_TEST[@]}"
do
echo "Testing for $SQL_FILE"
mysql -u $DB_USER --password=$DB_PASSWORD -e "DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME;"
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to create database $DB_NAME"
exit 1
fi
# Import the SQL file into the database. The file pathes are relative to the current script location
mysql -u $DB_USER --password=$DB_PASSWORD $DB_NAME < .github/assets/legacy_import/$SQL_FILE
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to import $SQL_FILE into database $DB_NAME"
exit 1
fi
# Run doctrine migrations, this will migrate the database to the current version. This process should not fail
php bin/console doctrine:migrations:migrate -n
# If the last command failed, exit the script
if [ $? -ne 0 ]; then
echo "Failed to migrate database $DB_NAME"
exit 1
fi
done

View File

@@ -1,54 +0,0 @@
name: "CodeQL"
on:
push:
branches: [master, ]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 14 * * 3'
jobs:
analyse:
name: Analyse
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -130,4 +130,12 @@ jobs:
- name: Test check-requirements command
run: php bin/console partdb:check-requirements -n
- name: Test legacy Part-DB import
run: bash .github/assets/legacy_import/test_legacy_import.sh
if: matrix.db-type == 'mysql' && matrix.php-versions == '8.2'
env:
DATABASE_URL: mysql://root:root@localhost:3306/legacy_db

View File

@@ -1 +1 @@
1.3.2
1.4.0

View File

@@ -44,6 +44,7 @@ export default class extends Controller
const title = this.element.dataset.deleteTitle;
const form = this.element;
const submitter = event.submitter;
const that = this;
const confirm = bootbox.confirm({
@@ -58,6 +59,14 @@ export default class extends Controller
const submit_btn = document.createElement('button');
submit_btn.type = 'submit';
submit_btn.style.display = 'none';
//If the clicked button has a value, set it on the submit button
if (submitter.value) {
submit_btn.value = submitter.value;
}
if (submitter.name) {
submit_btn.name = submitter.name;
}
form.appendChild(submit_btn);
submit_btn.click();
} else {

View File

@@ -0,0 +1,40 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Controller} from "@hotwired/stimulus";
import JSONFormatter from 'json-formatter-js';
/**
* This controller implements an element that renders a JSON object as a collapsible tree.
* The JSON object is passed as a data attribute.
* You have to apply the controller to a div element or similar block element which can contain other elements.
*/
export default class extends Controller {
connect() {
const depth_to_open = this.element.dataset.depthToOpen ?? 0;
const json_string = this.element.dataset.json;
const json_object = JSON.parse(json_string);
const formatter = new JSONFormatter(json_object, depth_to_open);
this.element.appendChild(formatter.render());
}
}

View File

@@ -105,4 +105,19 @@ form .col-form-label.required:after, form label.required:after {
position: relative;
right: -2px;
z-index: 700;
}
/****************************************
* HTML diff styling
****************************************/
/* Insertations are marked with green background and bold */
ins {
background-color: #95f095;
font-weight: bold;
}
del {
background-color: #f09595;
font-weight: bold;
}

View File

@@ -4,16 +4,17 @@
"require": {
"php": "^7.4 || ^8.0",
"ext-ctype": "*",
"ext-dom": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-dom": "*",
"beberlei/doctrineextensions": "^1.2",
"brick/math": "^0.8.15",
"composer/package-versions-deprecated": "1.11.99.4",
"doctrine/annotations": "^1.6",
"doctrine/data-fixtures": "^1.6.6",
"doctrine/dbal": "^3.4.6",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
@@ -25,6 +26,7 @@
"gregwar/captcha-bundle": "^2.1.0",
"hslavich/oneloginsaml-bundle": "^2.10",
"jbtronics/2fa-webauthn": "^1.0.0",
"jfcherng/php-diff": "^6.14",
"league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
@@ -33,7 +35,7 @@
"nyholm/psr7": "^1.1",
"ocramius/proxy-manager": "2.2.*",
"omines/datatables-bundle": "^0.5.0",
"php-translation/symfony-bundle": "^0.12.0",
"php-translation/symfony-bundle": "^0.13.0",
"phpdocumentor/reflection-docblock": "^5.2",
"s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^5.13",
@@ -82,7 +84,6 @@
},
"require-dev": {
"dama/doctrine-test-bundle": "^7.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"ekino/phpstan-banned-code": "^v1.0.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.4.7",
@@ -98,7 +99,8 @@
"symfony/stopwatch": "^5.2",
"symfony/web-profiler-bundle": "^5.2",
"symplify/easy-coding-standard": "^11.0",
"vimeo/psalm": "^5.6.0"
"vimeo/psalm": "^5.6.0",
"doctrine/doctrine-fixtures-bundle": "^3.2"
},
"suggest": {
"ext-bcmath": "Used to improve price calculation performance",

934
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -125,3 +125,9 @@ parameters:
env(DEFAULT_URI): 'https://partdb.changeme.invalid/'
env(SAML_ROLE_MAPPING): '{}'
env(HISTORY_SAVE_CHANGED_DATA): 1
env(HISTORY_SAVE_CHANGED_FIELDS): 1
env(HISTORY_SAVE_REMOVED_DATA): 1
env(HISTORY_SAVE_NEW_DATA): 1

View File

@@ -78,6 +78,7 @@ services:
$save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%'
$save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%'
$save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%'
$save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%'
tags:
- { name: 'doctrine.event_subscriber' }

View File

@@ -46,8 +46,9 @@ The following configuration options can only be changed by the server administra
### History/Eventlog related settings
The following options are used to configure, which (and how much) data is written to the system log:
* `HISTORY_SAVE_CHANGED_FIELDS`: When this option is set to true, the name of the fields which are changed, are saved to the DB (so for example it is logged that a user has changed, that the user has changed the name and description of the field, but not the data/content of these changes)
* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that a user has changed the name of a part and what the name was before). This can increase database size, when you have a lot of changes to enties.
* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that a user has changed the name of a part and what the name was before). This can increase database size, when you have a lot of changes to entities.
* `HISTORY_SAVE_REMOVED_DATA`: When this option is set to true, removed data is saved to log, meaning that you can easily undelete an entity, when it was removed accidentally.
* `HISTORY_SAVE_NEW_DATA`: When this option is set to true, the new data (the data after a change) is saved to element changed log entries. This allows you to easily see the changes between two revisions of an entity. This can increase database size, when you have a lot of changes to entities.
If you wanna use want to revert changes or view older revisions of entities, then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAVE_REMOVED_DATA` all have to be true.

View File

@@ -125,7 +125,8 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)');
$this->addSql('ALTER TABLE parts ADD CONSTRAINT parts_id_footprint_fk FOREIGN KEY (id_footprint) REFERENCES footprints (id)');
$this->addSql('ALTER TABLE parts ADD CONSTRAINT parts_id_manufacturer_fk FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id)');
if ($this->doesFKExists('attachment_types', 'attachement_types_parent_id_fk')) {
//We have to use the old table name here, because the if is executed before any sql command added by addSQL is executed (and thus the table name is not changed yet)
if ($this->doesFKExists('attachement_types', 'attachement_types_parent_id_fk')) {
$this->addSql('ALTER TABLE attachment_types DROP FOREIGN KEY attachement_types_parent_id_fk');
}
$this->addSql('ALTER TABLE attachment_types ADD filetype_filter LONGTEXT NOT NULL, ADD not_selectable TINYINT(1) NOT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE comment comment LONGTEXT NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL');
@@ -179,7 +180,8 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration
$this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)');
$this->addSql('ALTER TABLE suppliers ADD CONSTRAINT suppliers_parent_id_fk FOREIGN KEY (parent_id) REFERENCES suppliers (id)');
$this->addSql('DROP INDEX attachements_class_name_k ON attachments');
if ($this->doesFKExists('attachments', 'attachements_type_id_fk')) {
//We have to use the old table name here, because the if is executed before any sql command added by addSQL is executed (and thus the table name is not changed yet)
if ($this->doesFKExists('attachements', 'attachements_type_id_fk')) {
$this->addSql('ALTER TABLE attachments DROP FOREIGN KEY attachements_type_id_fk');
}
$this->addSql('ALTER TABLE attachments ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE type_id type_id INT DEFAULT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE filename filename VARCHAR(255) NOT NULL, CHANGE show_in_table show_in_table TINYINT(1) NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL');

View File

@@ -71,7 +71,10 @@ final class Version20220925162725 extends AbstractMultiPlatformMigration
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE INDEX attachment_types_idx_name ON attachment_types (name)');
$this->addSql('CREATE INDEX attachment_types_idx_parent_name ON attachment_types (parent_id, name)');
//We have to disable foreign key checks here, or we get a error on MySQL for legacy database migration. On MariaDB this works fine (with enabled foreign key checks), so I guess this is not so bad...
$this->addSql('SET foreign_key_checks = 0');
$this->addSql('ALTER TABLE attachments CHANGE type_id type_id INT NOT NULL');
$this->addSql('SET foreign_key_checks = 1');
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)');
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)');
$this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)');

View File

@@ -28,38 +28,38 @@
"build": "encore production --progress"
},
"dependencies": {
"@ckeditor/ckeditor5-alignment": "^36.0.0",
"@ckeditor/ckeditor5-autoformat": "^36.0.0",
"@ckeditor/ckeditor5-basic-styles": "^36.0.0",
"@ckeditor/ckeditor5-block-quote": "^36.0.0",
"@ckeditor/ckeditor5-code-block": "^36.0.0",
"@ckeditor/ckeditor5-dev-utils": "^33.0.0",
"@ckeditor/ckeditor5-alignment": "^37.1.0",
"@ckeditor/ckeditor5-autoformat": "^37.1.0",
"@ckeditor/ckeditor5-basic-styles": "^37.1.0",
"@ckeditor/ckeditor5-block-quote": "^37.1.0",
"@ckeditor/ckeditor5-code-block": "^37.1.0",
"@ckeditor/ckeditor5-dev-utils": "^37.0.0",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-editor-classic": "^36.0.0",
"@ckeditor/ckeditor5-essentials": "^36.0.0",
"@ckeditor/ckeditor5-find-and-replace": "^36.0.0",
"@ckeditor/ckeditor5-font": "^36.0.0",
"@ckeditor/ckeditor5-heading": "^36.0.0",
"@ckeditor/ckeditor5-highlight": "^36.0.0",
"@ckeditor/ckeditor5-horizontal-line": "^36.0.0",
"@ckeditor/ckeditor5-html-embed": "^36.0.0",
"@ckeditor/ckeditor5-html-support": "^36.0.0",
"@ckeditor/ckeditor5-image": "^36.0.0",
"@ckeditor/ckeditor5-indent": "^36.0.0",
"@ckeditor/ckeditor5-link": "^36.0.0",
"@ckeditor/ckeditor5-list": "^36.0.0",
"@ckeditor/ckeditor5-markdown-gfm": "^36.0.0",
"@ckeditor/ckeditor5-media-embed": "^36.0.0",
"@ckeditor/ckeditor5-paragraph": "^36.0.0",
"@ckeditor/ckeditor5-paste-from-office": "^36.0.0",
"@ckeditor/ckeditor5-remove-format": "^36.0.0",
"@ckeditor/ckeditor5-source-editing": "^36.0.0",
"@ckeditor/ckeditor5-special-characters": "^36.0.0",
"@ckeditor/ckeditor5-table": "^36.0.0",
"@ckeditor/ckeditor5-theme-lark": "^36.0.0",
"@ckeditor/ckeditor5-upload": "^36.0.0",
"@ckeditor/ckeditor5-watchdog": "^36.0.0",
"@ckeditor/ckeditor5-word-count": "^36.0.0",
"@ckeditor/ckeditor5-editor-classic": "^37.1.0",
"@ckeditor/ckeditor5-essentials": "^37.1.0",
"@ckeditor/ckeditor5-find-and-replace": "^37.1.0",
"@ckeditor/ckeditor5-font": "^37.1.0",
"@ckeditor/ckeditor5-heading": "^37.1.0",
"@ckeditor/ckeditor5-highlight": "^37.1.0",
"@ckeditor/ckeditor5-horizontal-line": "^37.1.0",
"@ckeditor/ckeditor5-html-embed": "^37.1.0",
"@ckeditor/ckeditor5-html-support": "^37.1.0",
"@ckeditor/ckeditor5-image": "^37.1.0",
"@ckeditor/ckeditor5-indent": "^37.1.0",
"@ckeditor/ckeditor5-link": "^37.1.0",
"@ckeditor/ckeditor5-list": "^37.1.0",
"@ckeditor/ckeditor5-markdown-gfm": "^37.1.0",
"@ckeditor/ckeditor5-media-embed": "^37.1.0",
"@ckeditor/ckeditor5-paragraph": "^37.1.0",
"@ckeditor/ckeditor5-paste-from-office": "^37.1.0",
"@ckeditor/ckeditor5-remove-format": "^37.1.0",
"@ckeditor/ckeditor5-source-editing": "^37.1.0",
"@ckeditor/ckeditor5-special-characters": "^37.1.0",
"@ckeditor/ckeditor5-table": "^37.1.0",
"@ckeditor/ckeditor5-theme-lark": "^37.1.0",
"@ckeditor/ckeditor5-upload": "^37.1.0",
"@ckeditor/ckeditor5-watchdog": "^37.1.0",
"@ckeditor/ckeditor5-word-count": "^37.1.0",
"@jbtronics/bs-treeview": "^1.0.1",
"bootbox": "^6.0.0",
"bootswatch": "^5.1.3",
@@ -73,15 +73,16 @@
"datatables.net-fixedheader-bs5": "^3.1.5",
"datatables.net-responsive-bs5": "^2.2.3",
"datatables.net-select-bs5": "^1.2.7",
"dompurify": "^2.0.6",
"dompurify": "^3.0.3",
"emoji.json": "^14.0.0",
"exports-loader": "^3.0.0",
"html5-qrcode": "^2.2.1",
"json-formatter-js": "^2.3.4",
"jszip": "^3.2.0",
"katex": "^0.16.0",
"marked": "^4.0.3",
"marked": "^4.3.0",
"pdfmake": "^0.2.2",
"stimulus-use": "^0.51.1",
"stimulus-use": "^0.52.0",
"tom-select": "^2.1.0",
"ts-loader": "^9.2.6",
"typescript": "^4.0.2"

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\DataTables\Column\LogEntryTargetColumn;
use App\DataTables\Filters\LogFilter;
use App\DataTables\LogDataTable;
use App\Entity\Base\AbstractDBElement;
@@ -33,6 +34,9 @@ use App\Entity\LogSystem\ElementEditedLogEntry;
use App\Form\Filters\LogFilterType;
use App\Repository\DBElementRepository;
use App\Services\LogSystem\EventUndoHelper;
use App\Services\LogSystem\LogEntryExtraFormatter;
use App\Services\LogSystem\LogLevelHelper;
use App\Services\LogSystem\LogTargetHelper;
use App\Services\LogSystem\TimeTravel;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
@@ -93,6 +97,51 @@ class LogController extends AbstractController
]);
}
/**
* @Route("/{id}/details", name="log_details")
* @param Request $request
* @param AbstractLogEntry $logEntry
* @return Response
*/
public function logDetails(Request $request, AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter,
LogLevelHelper $logLevelHelper, LogTargetHelper $logTargetHelper, EntityManagerInterface $entityManager): Response
{
$this->denyAccessUnlessGranted('show_details', $logEntry);
$extra_html = $logEntryExtraFormatter->format($logEntry);
$target_html = $logTargetHelper->formatTarget($logEntry);
$repo = $entityManager->getRepository(AbstractLogEntry::class);
$target_element = $repo->getTargetElement($logEntry);
return $this->render('log_system/details/log_details.html.twig', [
'log_entry' => $logEntry,
'target_element' => $target_element,
'extra_html' => $extra_html,
'target_html' => $target_html,
'log_level_helper' => $logLevelHelper,
]);
}
/**
* @Route("/{id}/delete", name="log_delete", methods={"DELETE"})
*/
public function deleteLogEntry(Request $request, AbstractLogEntry $logEntry, EntityManagerInterface $entityManager): RedirectResponse
{
$this->denyAccessUnlessGranted('delete', $logEntry);
if ($this->isCsrfTokenValid('delete'.$logEntry->getId(), $request->request->get('_token'))) {
//Remove part
$entityManager->remove($logEntry);
//Flush changes
$entityManager->flush();
$this->addFlash('success', 'log.delete.success');
}
return $this->redirectToRoute('homepage');
}
/**
* @Route("/undo", name="log_undo", methods={"POST"})
*/

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\DataTables\ErrorDataTable;
use App\DataTables\Filters\PartFilter;
use App\DataTables\Filters\PartSearchFilter;
use App\DataTables\PartsDataTable;
@@ -30,9 +31,11 @@ use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Exceptions\InvalidRegexException;
use App\Form\Filters\PartFilterType;
use App\Services\Parts\PartsTableActionHandler;
use App\Services\Trees\NodesListBuilder;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\EntityManagerInterface;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -41,6 +44,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
class PartListsController extends AbstractController
{
@@ -48,11 +52,14 @@ class PartListsController extends AbstractController
private NodesListBuilder $nodesListBuilder;
private DataTableFactory $dataTableFactory;
public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory)
private TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory, TranslatorInterface $translator)
{
$this->entityManager = $entityManager;
$this->nodesListBuilder = $nodesListBuilder;
$this->dataTableFactory = $dataTableFactory;
$this->translator = $translator;
}
/**
@@ -144,7 +151,21 @@ class PartListsController extends AbstractController
->handleRequest($request);
if ($table->isCallback()) {
return $table->getResponse();
try {
try {
return $table->getResponse();
} catch (DriverException $driverException) {
if ($driverException->getCode() === 1139) {
//Convert the driver exception to InvalidRegexException so it has the same hanlder as for SQLite
throw InvalidRegexException::fromDriverException($driverException);
} else {
throw $driverException;
}
}
} catch (InvalidRegexException $exception) {
$errors = $this->translator->trans('part.table.invalid_regex').': '.$exception->getReason();
return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors);
}
}
return $this->render($template, array_merge([
@@ -288,21 +309,22 @@ class PartListsController extends AbstractController
{
$filter = new PartSearchFilter($request->query->get('keyword', ''));
$filter->setName($request->query->getBoolean('name', true));
$filter->setCategory($request->query->getBoolean('category', true));
$filter->setDescription($request->query->getBoolean('description', true));
$filter->setMpn($request->query->getBoolean('mpn', true));
$filter->setTags($request->query->getBoolean('tags', true));
$filter->setStorelocation($request->query->getBoolean('storelocation', true));
$filter->setComment($request->query->getBoolean('comment', true));
$filter->setIPN($request->query->getBoolean('ipn', true));
$filter->setOrdernr($request->query->getBoolean('ordernr', true));
$filter->setSupplier($request->query->getBoolean('supplier', false));
$filter->setManufacturer($request->query->getBoolean('manufacturer', false));
$filter->setFootprint($request->query->getBoolean('footprint', false));
//As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)!
$filter->setName($request->query->getBoolean('name'));
$filter->setCategory($request->query->getBoolean('category'));
$filter->setDescription($request->query->getBoolean('description'));
$filter->setMpn($request->query->getBoolean('mpn'));
$filter->setTags($request->query->getBoolean('tags'));
$filter->setStorelocation($request->query->getBoolean('storelocation'));
$filter->setComment($request->query->getBoolean('comment'));
$filter->setIPN($request->query->getBoolean('ipn'));
$filter->setOrdernr($request->query->getBoolean('ordernr'));
$filter->setSupplier($request->query->getBoolean('supplier'));
$filter->setManufacturer($request->query->getBoolean('manufacturer'));
$filter->setFootprint($request->query->getBoolean('footprint'));
$filter->setRegex($request->query->getBoolean('regex', false));
$filter->setRegex($request->query->getBoolean('regex'));
return $filter;
}

View File

@@ -36,6 +36,7 @@ use App\Exceptions\EntityNotSupportedException;
use App\Repository\LogEntryRepository;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\LogSystem\LogTargetHelper;
use Doctrine\ORM\EntityManagerInterface;
use Omines\DataTablesBundle\Column\AbstractColumn;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -43,21 +44,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class LogEntryTargetColumn extends AbstractColumn
{
protected EntityManagerInterface $em;
protected LogEntryRepository $entryRepository;
protected EntityURLGenerator $entityURLGenerator;
protected ElementTypeNameGenerator $elementTypeNameGenerator;
protected TranslatorInterface $translator;
private LogTargetHelper $logTargetHelper;
public function __construct(EntityManagerInterface $entityManager, EntityURLGenerator $entityURLGenerator,
ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator)
public function __construct(LogTargetHelper $logTargetHelper)
{
$this->em = $entityManager;
$this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class);
$this->entityURLGenerator = $entityURLGenerator;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->translator = $translator;
$this->logTargetHelper = $logTargetHelper;
}
/**
@@ -80,71 +71,9 @@ class LogEntryTargetColumn extends AbstractColumn
public function render($value, $context): string
{
if ($context instanceof UserNotAllowedLogEntry && $this->options['showAccessDeniedPath']) {
return htmlspecialchars($context->getPath());
}
/** @var AbstractLogEntry $context */
$target = $this->entryRepository->getTargetElement($context);
$tmp = '';
//The element is existing
if ($target instanceof NamedElementInterface && !empty($target->getName())) {
try {
$tmp = sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($target),
$this->elementTypeNameGenerator->getTypeNameCombination($target, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp = $this->elementTypeNameGenerator->getTypeNameCombination($target, true);
}
} elseif ($target instanceof AbstractDBElement) { //Target does not have a name
$tmp = sprintf(
'<i>%s</i>: %s',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($target),
$target->getID()
);
} elseif (null === $target && $context->hasTarget()) { //Element was deleted
$tmp = sprintf(
'<i>%s</i>: %s [%s]',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()),
$context->getTargetID(),
$this->translator->trans('log.target_deleted')
);
}
//Add a hint to the associated element if possible
if (null !== $target && $this->options['show_associated']) {
if ($target instanceof Attachment && null !== $target->getElement()) {
$on = $target->getElement();
} elseif ($target instanceof AbstractParameter && null !== $target->getElement()) {
$on = $target->getElement();
} elseif ($target instanceof PartLot && null !== $target->getPart()) {
$on = $target->getPart();
} elseif ($target instanceof Orderdetail && null !== $target->getPart()) {
$on = $target->getPart();
} elseif ($target instanceof Pricedetail && null !== $target->getOrderdetail() && null !== $target->getOrderdetail()->getPart()) {
$on = $target->getOrderdetail()->getPart();
} elseif ($target instanceof ProjectBOMEntry && null !== $target->getProject()) {
$on = $target->getProject();
}
if (isset($on) && is_object($on)) {
try {
$tmp .= sprintf(
' (<a href="%s">%s</a>)',
$this->entityURLGenerator->infoURL($on),
$this->elementTypeNameGenerator->getTypeNameCombination($on, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp .= ' ('.$this->elementTypeNameGenerator->getTypeNameCombination($target, true).')';
}
}
}
//Log is not associated with an element
return $tmp;
return $this->logTargetHelper->formatTarget($context, [
'showAccessDeniedPath' => $this->options['showAccessDeniedPath'],
'show_associated' => $this->options['show_associated'],
]);
}
}

View File

@@ -73,13 +73,13 @@ class RevertLogColumn extends AbstractColumn
{
if (
$context instanceof CollectionElementDeleted
|| ($context instanceof ElementDeletedLogEntry && $context->hasOldDataInformations())
|| ($context instanceof ElementDeletedLogEntry && $context->hasOldDataInformation())
) {
$icon = 'fa-trash-restore';
$title = $this->translator->trans('log.undo.undelete');
} elseif (
$context instanceof ElementCreatedLogEntry
|| ($context instanceof ElementEditedLogEntry && $context->hasOldDataInformations())
|| ($context instanceof ElementEditedLogEntry && $context->hasOldDataInformation())
) {
$icon = 'fa-undo';
$title = $this->translator->trans('log.undo.undo');

View File

@@ -0,0 +1,85 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\DataTables;
use App\DataTables\Column\RowClassColumn;
use App\Entity\Parts\Part;
use Omines\DataTablesBundle\Adapter\ArrayAdapter;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableFactory;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ErrorDataTable implements DataTableTypeInterface
{
public function configureOptions(OptionsResolver $optionsResolver): void
{
$optionsResolver->setRequired('errors');
$optionsResolver->setAllowedTypes('errors', ['array', 'string']);
$optionsResolver->setNormalizer('errors', function (OptionsResolver $optionsResolver, $errors) {
if (is_string($errors)) {
$errors = [$errors];
}
return $errors;
});
}
public function configure(DataTable $dataTable, array $options)
{
$optionsResolver = new OptionsResolver();
$this->configureOptions($optionsResolver);
$options = $optionsResolver->resolve($options);
$dataTable
->add('dont_matter_we_only_set_color', RowClassColumn::class, [
'render' => function ($value, $context) {
return 'table-warning';
},
])
->add('error', TextColumn::class, [
'label' => 'error_table.error',
'render' => function ($value, $context) {
return '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . $value;
},
])
;
//Build the array containing data
$data = [];
foreach ($options['errors'] as $error) {
$data[] = ['error' => $error];
}
$dataTable->createAdapter(ArrayAdapter::class, $data);
}
public static function errorTable(DataTableFactory $dataTableFactory, Request $request, $errors): Response
{
$error_table = $dataTableFactory->createFromType(self::class, ['errors' => $errors]);
$error_table->handleRequest($request);
return $error_table->getResponse();
}
}

View File

@@ -86,6 +86,9 @@ class PartSearchFilter implements FilterInterface
if($this->description) {
$fields_to_search[] = 'part.description';
}
if ($this->comment) {
$fields_to_search[] = 'part.comment';
}
if($this->tags) {
$fields_to_search[] = 'part.tags';
}

View File

@@ -43,6 +43,7 @@ use App\Exceptions\EntityNotSupportedException;
use App\Repository\LogEntryRepository;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\LogSystem\LogLevelHelper;
use App\Services\UserSystem\UserAvatarHelper;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
@@ -67,10 +68,11 @@ class LogDataTable implements DataTableTypeInterface
protected LogEntryRepository $logRepo;
protected Security $security;
protected UserAvatarHelper $userAvatarHelper;
protected LogLevelHelper $logLevelHelper;
public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator, EntityURLGenerator $entityURLGenerator, EntityManagerInterface $entityManager,
Security $security, UserAvatarHelper $userAvatarHelper)
Security $security, UserAvatarHelper $userAvatarHelper, LogLevelHelper $logLevelHelper)
{
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->translator = $translator;
@@ -79,6 +81,7 @@ class LogDataTable implements DataTableTypeInterface
$this->logRepo = $entityManager->getRepository(AbstractLogEntry::class);
$this->security = $security;
$this->userAvatarHelper = $userAvatarHelper;
$this->logLevelHelper = $logLevelHelper;
}
public function configureOptions(OptionsResolver $optionsResolver): void
@@ -112,69 +115,18 @@ class LogDataTable implements DataTableTypeInterface
//This special $$rowClass column is used to set the row class depending on the log level. The class gets set by the frontend controller
$dataTable->add('dont_matter', RowClassColumn::class, [
'render' => static function ($value, AbstractLogEntry $context) {
switch ($context->getLevel()) {
case AbstractLogEntry::LEVEL_EMERGENCY:
case AbstractLogEntry::LEVEL_ALERT:
case AbstractLogEntry::LEVEL_CRITICAL:
case AbstractLogEntry::LEVEL_ERROR:
return 'table-danger';
case AbstractLogEntry::LEVEL_WARNING:
return 'table-warning';
case AbstractLogEntry::LEVEL_NOTICE:
return 'table-info';
default:
return '';
}
'render' => function ($value, AbstractLogEntry $context) {
return $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString());
},
]);
$dataTable->add('symbol', TextColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => static function ($value, AbstractLogEntry $context) {
switch ($context->getLevelString()) {
case LogLevel::DEBUG:
$symbol = 'fa-bug';
break;
case LogLevel::INFO:
$symbol = 'fa-info';
break;
case LogLevel::NOTICE:
$symbol = 'fa-flag';
break;
case LogLevel::WARNING:
$symbol = 'fa-exclamation-circle';
break;
case LogLevel::ERROR:
$symbol = 'fa-exclamation-triangle';
break;
case LogLevel::CRITICAL:
$symbol = 'fa-bolt';
break;
case LogLevel::ALERT:
$symbol = 'fa-radiation';
break;
case LogLevel::EMERGENCY:
$symbol = 'fa-skull-crossbones';
break;
default:
$symbol = 'fa-question-circle';
break;
}
'render' => function ($value, AbstractLogEntry $context) {
return sprintf(
'<i class="fas fa-fw %s" title="%s"></i>',
$symbol,
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
$context->getLevelString()
);
},
@@ -188,6 +140,12 @@ class LogDataTable implements DataTableTypeInterface
$dataTable->add('timestamp', LocaleDateTimeColumn::class, [
'label' => 'log.timestamp',
'timeFormat' => 'medium',
'render' => function (string $value, AbstractLogEntry $context) {
return sprintf('<a href="%s">%s</a>',
$this->urlGenerator->generate('log_details', ['id' => $context->getId()]),
$value
);
}
]);
$dataTable->add('type', TextColumn::class, [
@@ -278,7 +236,7 @@ class LogDataTable implements DataTableTypeInterface
'href' => function ($value, AbstractLogEntry $context) {
if (
($context instanceof TimeTravelInterface
&& $context->hasOldDataInformations())
&& $context->hasOldDataInformation())
|| $context instanceof CollectionElementDeleted
) {
try {

View File

@@ -20,6 +20,7 @@
namespace App\Doctrine;
use App\Exceptions\InvalidRegexException;
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
@@ -43,7 +44,11 @@ class SQLiteRegexExtension implements EventSubscriberInterface
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) {
$native_connection->sqliteCreateFunction('REGEXP', function ($pattern, $value) {
return (false !== mb_ereg($pattern, $value)) ? 1 : 0;
try {
return (false !== mb_ereg($pattern, $value)) ? 1 : 0;
} catch (\ErrorException $e) {
throw InvalidRegexException::fromMBRegexError($e);
}
});
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Entity\Contracts;
interface LogWithNewDataInterface
{
/**
* Checks if this entry has information about the new data.
* @return bool
*/
public function hasNewDataInformation(): bool;
/**
* Returns the new data for this entry.
*/
public function getNewData(): array;
/**
* Sets the new data for this entry.
* @return $this
*/
public function setNewData(array $new_data): self;
}

View File

@@ -31,7 +31,7 @@ interface TimeTravelInterface
*
* @return bool true if this entry has information about the changed data
*/
public function hasOldDataInformations(): bool;
public function hasOldDataInformation(): bool;
/**
* Returns the data the entity had before this log entry.

View File

@@ -88,7 +88,7 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter
return $this;
}
public function hasOldDataInformations(): bool
public function hasOldDataInformation(): bool
{
return !empty($this->extra['o']);
}

View File

@@ -25,6 +25,7 @@ namespace App\Entity\LogSystem;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\LogWithCommentInterface;
use App\Entity\Contracts\LogWithEventUndoInterface;
use App\Entity\Contracts\LogWithNewDataInterface;
use App\Entity\Contracts\TimeTravelInterface;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
@@ -32,7 +33,7 @@ use InvalidArgumentException;
/**
* @ORM\Entity()
*/
class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface
class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface, LogWithNewDataInterface
{
protected string $typeString = 'element_edited';
@@ -49,7 +50,7 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf
*/
public function hasChangedFieldsInfo(): bool
{
return isset($this->extra['f']) || $this->hasOldDataInformations();
return isset($this->extra['f']) || $this->hasOldDataInformation();
}
/**
@@ -59,7 +60,7 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf
*/
public function getChangedFields(): array
{
if ($this->hasOldDataInformations()) {
if ($this->hasOldDataInformation()) {
return array_keys($this->getOldData());
}
@@ -92,7 +93,29 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf
return $this;
}
public function hasOldDataInformations(): bool
public function hasNewDataInformation(): bool
{
return !empty($this->extra['n']);
}
public function getNewData(): array
{
return $this->extra['n'] ?? [];
}
/**
* Sets the old data for this entry.
*
* @return $this
*/
public function setNewData(array $new_data): self
{
$this->extra['n'] = $new_data;
return $this;
}
public function hasOldDataInformation(): bool
{
return !empty($this->extra['d']);
}

View File

@@ -113,7 +113,7 @@ class SecurityEventLogEntry extends AbstractLogEntry
{
$key = $this->extra['e'];
return static::SECURITY_TYPE_MAPPING[$key] ?? 'unkown';
return static::SECURITY_TYPE_MAPPING[$key] ?? 'unknown';
}
/**

View File

@@ -78,11 +78,12 @@ class EventLoggerSubscriber implements EventSubscriber
protected bool $save_changed_fields;
protected bool $save_changed_data;
protected bool $save_removed_data;
protected bool $save_new_data;
protected PropertyAccessorInterface $propertyAccessor;
public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper,
bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor,
EventUndoHelper $eventUndoHelper)
bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, bool $save_new_data,
PropertyAccessorInterface $propertyAccessor, EventUndoHelper $eventUndoHelper)
{
$this->logger = $logger;
$this->serializer = $serializer;
@@ -93,6 +94,8 @@ class EventLoggerSubscriber implements EventSubscriber
$this->save_changed_fields = $save_changed_fields;
$this->save_changed_data = $save_changed_data;
$this->save_removed_data = $save_removed_data;
//This option only makes sense if save_changed_data is true
$this->save_new_data = $save_new_data && $save_changed_data;
}
public function onFlush(OnFlushEventArgs $eventArgs): void
@@ -150,6 +153,7 @@ class EventLoggerSubscriber implements EventSubscriber
$log->setTargetElementID($undoEvent->getDeletedElementID());
}
}
$this->logger->log($log);
}
}
@@ -301,6 +305,24 @@ class EventLoggerSubscriber implements EventSubscriber
}, ARRAY_FILTER_USE_BOTH);
}
/**
* Restrict the length of every string in the given array to MAX_STRING_LENGTH, to save memory in the case of very
* long strings (e.g. images in notes)
* @param array $fields
* @return array
*/
protected function fieldLengthRestrict(array $fields): array
{
return array_map(
static function ($value) {
if (is_string($value)) {
return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
}
return $value;
}, $fields);
}
protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void
{
$uow = $em->getUnitOfWork();
@@ -314,20 +336,24 @@ class EventLoggerSubscriber implements EventSubscriber
} else { //Otherwise we have to get it from entity changeset
$changeSet = $uow->getEntityChangeSet($entity);
$old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
//If save_new_data is enabled, we extract it from the change set
if ($this->save_new_data) {
$new_data = array_combine(array_keys($changeSet), array_column($changeSet, 1));
}
}
$old_data = $this->filterFieldRestrictions($entity, $old_data);
//Restrict length of string fields, to save memory...
$old_data = array_map(
static function ($value) {
if (is_string($value)) {
return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
}
return $value;
}, $old_data);
$old_data = $this->fieldLengthRestrict($old_data);
$logEntry->setOldData($old_data);
if (!empty($new_data)) {
$new_data = $this->filterFieldRestrictions($entity, $new_data);
$new_data = $this->fieldLengthRestrict($new_data);
$logEntry->setNewData($new_data);
}
}
/**

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Exceptions;
use Doctrine\DBAL\Exception\DriverException;
use ErrorException;
class InvalidRegexException extends \RuntimeException
{
private ?string $reason;
public function __construct(string $reason = null)
{
$this->reason = $reason;
parent::__construct('Invalid regular expression');
}
/**
* Returns the reason for the exception (what the regex driver deemed invalid)
* @return string|null
*/
public function getReason(): ?string
{
return $this->reason;
}
/**
* Creates a new exception from a driver exception happening, when MySQL encounters an invalid regex
* @param DriverException $exception
* @return self
*/
public static function fromDriverException(DriverException $exception): self
{
//1139 means invalid regex error
if ($exception->getCode() !== 1139) {
throw new \InvalidArgumentException('The given exception is not a driver exception', 0, $exception);
}
//Reason is the part after the erorr code
$reason = preg_replace('/^.*1139 /', '', $exception->getMessage());
return new self($reason);
}
/**
* Creates a new exception from the errorException thrown by mb_ereg
* @param ErrorException $ex
* @return self
*/
public static function fromMBRegexError(ErrorException $ex): self
{
//Ensure that the error is really a mb_ereg error
if ($ex->getSeverity() !== E_WARNING || !strpos($ex->getMessage(), 'mb_ereg()') !== false) {
throw new \InvalidArgumentException('The given exception is not a mb_ereg error', 0, $ex);
}
//Reason is the part after the erorr code
$reason = preg_replace('/^.*mb_ereg\(\): /', '', $ex->getMessage());
return new self($reason);
}
}

View File

@@ -23,9 +23,22 @@ declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\Attachments\CategoryAttachment;
use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Attachments\FootprintAttachment;
use App\Entity\Attachments\GroupAttachment;
use App\Entity\Attachments\ManufacturerAttachment;
use App\Entity\Attachments\MeasurementUnitAttachment;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Attachments\ProjectAttachment;
use App\Entity\Attachments\StorelocationAttachment;
use App\Entity\Attachments\SupplierAttachment;
use App\Entity\Attachments\UserAttachment;
use App\Entity\UserSystem\User;
use App\Services\UserSystem\PermissionManager;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Component\Security\Core\Security;
use function in_array;
@@ -50,26 +63,78 @@ class AttachmentVoter extends ExtendedVoter
{
//return $this->resolver->inherit($user, 'attachments', $attribute) ?? false;
//If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element
$target_element = $subject->getElement();
if (! $subject instanceof Attachment || null === $target_element) {
//This voter only works for attachments
if (!is_a($subject, Attachment::class, true)) {
return false;
}
//Depending on the operation delegate either to the attachments element or to the attachment permission
if ($attribute === 'show_private') {
return $this->resolver->inherit($user, 'attachments', 'show_private') ?? false;
}
if (is_object($subject)) {
//If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element
$target_element = $subject->getElement();
if ($target_element) {
return $this->security->isGranted($this->mapOperation($attribute), $target_element);
}
}
if (is_string($subject)) {
//If we do not have a concrete element (or we just got a string as value), we delegate to the different categories
if (is_a($subject, AttachmentTypeAttachment::class, true)) {
$param = 'attachment_types';
} elseif (is_a($subject, CategoryAttachment::class, true)) {
$param = 'categories';
} elseif (is_a($subject, CurrencyAttachment::class, true)) {
$param = 'currencies';
} elseif (is_a($subject, ProjectAttachment::class, true)) {
$param = 'projects';
} elseif (is_a($subject, FootprintAttachment::class, true)) {
$param = 'footprints';
} elseif (is_a($subject, GroupAttachment::class, true)) {
$param = 'groups';
} elseif (is_a($subject, ManufacturerAttachment::class, true)) {
$param = 'manufacturers';
} elseif (is_a($subject, MeasurementUnitAttachment::class, true)) {
$param = 'measurement_units';
} elseif (is_a($subject, PartAttachment::class, true)) {
$param = 'parts';
} elseif (is_a($subject, StorelocationAttachment::class, true)) {
$param = 'storelocations';
} elseif (is_a($subject, SupplierAttachment::class, true)) {
$param = 'suppliers';
} elseif (is_a($subject, UserAttachment::class, true)) {
$param = 'users';
} elseif ($subject === Attachment::class) {
//If the subject was deleted, we can not determine the type properly, so we just use the parts permission
$param = 'parts';
}
else {
throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? get_class($subject) : $subject));
}
return $this->resolver->inherit($user, $param, $this->mapOperation($attribute)) ?? false;
}
return false;
}
private function mapOperation(string $attribute): string
{
switch ($attribute) {
//We can view the attachment if we can view the element
case 'read':
case 'view':
return $this->security->isGranted('read', $target_element);
return 'read';
//We can edit/create/delete the attachment if we can edit the element
case 'edit':
case 'create':
case 'delete':
return $this->security->isGranted('edit', $target_element);
case 'show_private':
return $this->resolver->inherit($user, 'attachments', 'show_private') ?? false;
return 'edit';
case 'show_history':
return 'show_history';
}
throw new \RuntimeException('Encountered unknown attribute "'.$attribute.'" in AttachmentVoter!');
@@ -87,7 +152,7 @@ class AttachmentVoter extends ExtendedVoter
{
if (is_a($subject, Attachment::class, true)) {
//These are the allowed attributes
return in_array($attribute, ['read', 'view', 'edit', 'delete', 'create', 'show_private'], true);
return in_array($attribute, ['read', 'view', 'edit', 'delete', 'create', 'show_private', 'show_history'], true);
}
//Allow class name as subject

View File

@@ -24,13 +24,28 @@ namespace App\Security\Voter;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Entity\UserSystem\User;
use App\Services\UserSystem\PermissionManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
class LogEntryVoter extends ExtendedVoter
{
public const ALLOWED_OPS = ['read', 'delete'];
public const ALLOWED_OPS = ['read', 'show_details', 'delete'];
private Security $security;
public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security)
{
parent::__construct($resolver, $entityManager);
$this->security = $security;
}
protected function voteOnUser(string $attribute, $subject, User $user): bool
{
if (!$subject instanceof AbstractLogEntry) {
throw new \InvalidArgumentException('The subject must be an instance of '.AbstractLogEntry::class);
}
if ('delete' === $attribute) {
return $this->resolver->inherit($user, 'system', 'delete_logs') ?? false;
}
@@ -47,13 +62,24 @@ class LogEntryVoter extends ExtendedVoter
return $this->resolver->inherit($user, 'system', 'show_logs') ?? false;
}
if ('show_details' === $attribute) {
//To view details of a element related log entry, the user needs to be able to view the history of this entity type
$targetClass = $subject->getTargetClass();
if (null !== $targetClass) {
return $this->security->isGranted('show_history', $targetClass) ?? false;
}
//In other cases, this behaves like the read permission
return $this->voteOnUser('read', $subject, $user);
}
return false;
}
protected function supports($attribute, $subject): bool
{
if ($subject instanceof AbstractLogEntry) {
return in_array($subject, static::ALLOWED_OPS, true);
return in_array($attribute, static::ALLOWED_OPS, true);
}
return false;

View File

@@ -53,66 +53,72 @@ class ParameterVoter extends ExtendedVoter
{
//return $this->resolver->inherit($user, 'attachments', $attribute) ?? false;
if (!$subject instanceof AbstractParameter) {
if (!is_a($subject, AbstractParameter::class, true)) {
return false;
}
//If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element
$target_element = $subject->getElement();
if ($target_element !== null) {
//Depending on the operation delegate either to the attachments element or to the attachment permission
if (is_object($subject)) {
//If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element
$target_element = $subject->getElement();
if ($target_element !== null) {
//Depending on the operation delegate either to the attachments element or to the attachment permission
switch ($attribute) {
//We can view the attachment if we can view the element
case 'read':
case 'view':
$operation = 'read';
break;
//We can edit/create/delete the attachment if we can edit the element
case 'edit':
case 'create':
case 'delete':
$operation = 'edit';
break;
case 'show_history':
$operation = 'show_history';
break;
case 'revert_element':
$operation = 'revert_element';
break;
default:
throw new RuntimeException('Unknown operation: '.$attribute);
switch ($attribute) {
//We can view the attachment if we can view the element
case 'read':
case 'view':
$operation = 'read';
break;
//We can edit/create/delete the attachment if we can edit the element
case 'edit':
case 'create':
case 'delete':
$operation = 'edit';
break;
case 'show_history':
$operation = 'show_history';
break;
case 'revert_element':
$operation = 'revert_element';
break;
default:
throw new RuntimeException('Unknown operation: '.$attribute);
}
return $this->security->isGranted($operation, $target_element);
}
return $this->security->isGranted($operation, $target_element);
}
//If we do not have a concrete element, we delegate to the different categories
if ($subject instanceof AttachmentTypeParameter) {
//If we do not have a concrete element (or we just got a string as value), we delegate to the different categories
if (is_a($subject, AttachmentTypeParameter::class, true)) {
$param = 'attachment_types';
} elseif ($subject instanceof CategoryParameter) {
} elseif (is_a($subject, CategoryParameter::class, true)) {
$param = 'categories';
} elseif ($subject instanceof CurrencyParameter) {
} elseif (is_a($subject, CurrencyParameter::class, true)) {
$param = 'currencies';
} elseif ($subject instanceof ProjectParameter) {
} elseif (is_a($subject, ProjectParameter::class, true)) {
$param = 'projects';
} elseif ($subject instanceof FootprintParameter) {
} elseif (is_a($subject, FootprintParameter::class, true)) {
$param = 'footprints';
} elseif ($subject instanceof GroupParameter) {
} elseif (is_a($subject, GroupParameter::class, true)) {
$param = 'groups';
} elseif ($subject instanceof ManufacturerParameter) {
} elseif (is_a($subject, ManufacturerParameter::class, true)) {
$param = 'manufacturers';
} elseif ($subject instanceof MeasurementUnitParameter) {
} elseif (is_a($subject, MeasurementUnitParameter::class, true)) {
$param = 'measurement_units';
} elseif ($subject instanceof PartParameter) {
} elseif (is_a($subject, PartParameter::class, true)) {
$param = 'parts';
} elseif ($subject instanceof StorelocationParameter) {
} elseif (is_a($subject, StorelocationParameter::class, true)) {
$param = 'storelocations';
} elseif ($subject instanceof SupplierParameter) {
} elseif (is_a($subject, SupplierParameter::class, true)) {
$param = 'suppliers';
} else {
throw new RuntimeException('Encountered unknown Parameter type: ' . get_class($subject));
} elseif ($subject === AbstractParameter::class) {
//If the subject was deleted, we can not determine the type properly, so we just use the parts permission
$param = 'parts';
}
else {
throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? get_class($subject) : $subject));
}
return $this->resolver->inherit($user, $param, $attribute) ?? false;

View File

@@ -24,6 +24,7 @@ namespace App\Services;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\ProjectSystem\Project;
use App\Entity\LabelSystem\LabelProfile;
@@ -43,17 +44,20 @@ use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use App\Exceptions\EntityNotSupportedException;
use Doctrine\ORM\Mapping\Entity;
use function get_class;
use Symfony\Contracts\Translation\TranslatorInterface;
class ElementTypeNameGenerator
{
protected TranslatorInterface $translator;
private EntityURLGenerator $entityURLGenerator;
protected array $mapping;
public function __construct(TranslatorInterface $translator)
public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator)
{
$this->translator = $translator;
$this->entityURLGenerator = $entityURLGenerator;
//Child classes has to become before parent classes
$this->mapping = [
@@ -132,4 +136,81 @@ class ElementTypeNameGenerator
return $type.': '.$entity->getName();
}
/**
* Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and
* "Type: ID" (on elements without a name). If possible the value is given as a link to the element.
* @param AbstractDBElement $entity The entity for which the label should be generated
* @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information
* @return string
*/
public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $include_associated = false): string
{
//The element is existing
if ($entity instanceof NamedElementInterface && !empty($entity->getName())) {
try {
$tmp = sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($entity),
$this->getTypeNameCombination($entity, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp = $this->getTypeNameCombination($entity, true);
}
} else { //Target does not have a name
$tmp = sprintf(
'<i>%s</i>: %s',
$this->getLocalizedTypeLabel($entity),
$entity->getID()
);
}
//Add a hint to the associated element if possible
if ($include_associated) {
if ($entity instanceof Attachment && null !== $entity->getElement()) {
$on = $entity->getElement();
} elseif ($entity instanceof AbstractParameter && null !== $entity->getElement()) {
$on = $entity->getElement();
} elseif ($entity instanceof PartLot && null !== $entity->getPart()) {
$on = $entity->getPart();
} elseif ($entity instanceof Orderdetail && null !== $entity->getPart()) {
$on = $entity->getPart();
} elseif ($entity instanceof Pricedetail && null !== $entity->getOrderdetail() && null !== $entity->getOrderdetail()->getPart()) {
$on = $entity->getOrderdetail()->getPart();
} elseif ($entity instanceof ProjectBOMEntry && null !== $entity->getProject()) {
$on = $entity->getProject();
}
if (isset($on) && is_object($on)) {
try {
$tmp .= sprintf(
' (<a href="%s">%s</a>)',
$this->entityURLGenerator->infoURL($on),
$this->getTypeNameCombination($on, true)
);
} catch (EntityNotSupportedException $exception) {
}
}
}
return $tmp;
}
/**
* Create a HTML formatted label for a deleted element of which we only know the class and the ID.
* Please note that it is not checked if the element really not exists anymore, so you have to do this yourself.
* @param string $class
* @param int $id
* @return string
*/
public function formatElementDeletedHTML(string $class, int $id): string
{
return sprintf(
'<i>%s</i>: %s [%s]',
$this->getLocalizedTypeLabel($class),
$id,
$this->translator->trans('log.target_deleted')
);
}
}

View File

@@ -69,7 +69,10 @@ class MySQLDumpXMLConverter
//Iterate over all <table> nodes and convert them to arrays
foreach ($tables as $table) {
$table_data[$table->getAttribute('name')] = $this->convertTableToArray($table);
//Normalize the table name to lowercase. On Linux filesystems the tables sometimes contain uppercase letters
//However we expect the table names to be lowercase in the further steps
$table_name = strtolower($table->getAttribute('name'));
$table_data[$table_name] = $this->convertTableToArray($table);
}
return $table_data;

View File

@@ -84,7 +84,15 @@ trait PKImportHelperTrait
//Determine file extension (if the extension is empty, we use the original extension)
if (empty($attachment_row['extension'])) {
$attachment_row['extension'] = pathinfo($attachment_row['originalname'], PATHINFO_EXTENSION);
//Use mime type to determine the extension like PartKeepr does in legacy implementation (just use the second part of the mime type)
//See UploadedFile.php:291 in PartKeepr (https://github.com/partkeepr/PartKeepr/blob/f6176c3354b24fa39ac8bc4328ee0df91de3d5b6/src/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php#L291)
if (!empty ($attachment_row['mimetype'])) {
$attachment_row['extension'] = explode('/', $attachment_row['mimetype'])[1];
} else {
//If the mime type is empty, we use the original extension
$attachment_row['extension'] = pathinfo($attachment_row['originalname'], PATHINFO_EXTENSION);
}
}
//Determine file path

View File

@@ -30,10 +30,12 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use Brick\Math\BigDecimal;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Intl\Currencies;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
@@ -43,10 +45,13 @@ class PKPartImporter
{
use PKImportHelperTrait;
public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor)
private string $base_currency;
public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor, string $default_currency)
{
$this->em = $em;
$this->propertyAccessor = $propertyAccessor;
$this->base_currency = $default_currency;
}
public function importParts(array $data): int
@@ -168,11 +173,21 @@ class PKPartImporter
$entity->setName($name);
$entity->setValueText($partparameter['stringValue'] ?? '');
$entity->setUnit($this->getUnitSymbol($data, (int) $partparameter['unit_id']));
if ($partparameter['unit_id'] === null) {
$entity->setUnit($this->getUnitSymbol($data, (int)$partparameter['unit_id']));
} else {
$entity->setUnit("");
}
$entity->setValueMin($partparameter['normalizedMinValue'] ?? null);
$entity->setValueTypical($partparameter['normalizedValue'] ?? null);
$entity->setValueMax($partparameter['normalizedMaxValue'] ?? null);
if ($partparameter['normalizedMinValue'] !== null) {
$entity->setValueMin((float) $partparameter['normalizedMinValue']);
}
if ($partparameter['normalizedValue'] !== null) {
$entity->setValueTypical((float) $partparameter['normalizedValue']);
}
if ($partparameter['normalizedMaxValue'] !== null) {
$entity->setValueMax((float) $partparameter['normalizedMaxValue']);
}
$part = $this->em->find(Part::class, (int) $partparameter['part_id']);
if (!$part) {
@@ -185,6 +200,37 @@ class PKPartImporter
$this->em->flush();
}
/**
* Returns the currency for the given ISO code. If the currency does not exist, it is created.
* This function returns null if the ISO code is the base currency.
* @param string $currency_iso_code
* @return Currency|null
*/
protected function getOrCreateCurrency(string $currency_iso_code): ?Currency
{
//Normalize ISO code
$currency_iso_code = strtoupper($currency_iso_code);
//We do not have a currency for the base currency to be consistent with prices without currencies
if ($currency_iso_code === $this->base_currency) {
return null;
}
$currency = $this->em->getRepository(Currency::class)->findOneBy([
'iso_code' => $currency_iso_code,
]);
if (!$currency) {
$currency = new Currency();
$currency->setIsoCode($currency_iso_code);
$currency->setName(Currencies::getName($currency_iso_code));
$this->em->persist($currency);
$this->em->flush();
}
return $currency;
}
protected function importOrderdetails(array $data): void
{
if (!isset($data['partdistributor'])) {
@@ -226,6 +272,7 @@ class PKPartImporter
$orderdetail->setSupplier($supplier);
$orderdetail->setSupplierpartnr($spn);
$part->addOrderdetail($orderdetail);
$this->em->persist($orderdetail);
}
//Add the price information to the orderdetail
@@ -234,12 +281,25 @@ class PKPartImporter
$orderdetail->addPricedetail($pricedetail);
//Partkeepr stores the price per item, we need to convert it to the price per packaging unit
$price_per_item = BigDecimal::of($partdistributor['price']);
$pricedetail->setPrice($price_per_item->multipliedBy($partdistributor['packagingUnit']));
$pricedetail->setPriceRelatedQuantity($partdistributor['packagingUnit'] ?? 1);
$packaging_unit = $partdistributor['packagingUnit'] ?? 1;
$pricedetail->setPrice($price_per_item->multipliedBy($packaging_unit));
$pricedetail->setPriceRelatedQuantity($packaging_unit);
//We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept)
//But in Part-DB the minimum discount qty have to be unique across a orderdetail
$pricedetail->setMinDiscountQuantity($packaging_unit);
//Set the currency of the price
if (!empty($partdistributor['currency'])) {
$currency = $this->getOrCreateCurrency($partdistributor['currency']);
$pricedetail->setCurrency($currency);
}
$this->em->persist($pricedetail);
}
//We have to flush the changes in every loop, so the find function can find newly created entities
$this->em->flush();
//Clear the entity manager to improve performance
$this->em->clear();
}
}

View File

@@ -0,0 +1,163 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LogSystem;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Services\ElementTypeNameGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class LogDataFormatter
{
private const STRING_MAX_LENGTH = 1024;
private TranslatorInterface $translator;
private EntityManagerInterface $entityManager;
private ElementTypeNameGenerator $elementTypeNameGenerator;
public function __construct(TranslatorInterface $translator, EntityManagerInterface $entityManager, ElementTypeNameGenerator $elementTypeNameGenerator)
{
$this->translator = $translator;
$this->entityManager = $entityManager;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
}
/**
* Formats the given data of a log entry as HTML
* @param mixed $data
* @param AbstractLogEntry $logEntry
* @param string $fieldName
* @return string
*/
public function formatData($data, AbstractLogEntry $logEntry, string $fieldName): string
{
if (is_string($data)) {
$tmp = '<span class="text-muted user-select-none">"</span>' . mb_strimwidth(htmlspecialchars($data), 0, self::STRING_MAX_LENGTH, ) . '<span class="text-muted user-select-none">"</span>';
//Show special characters and line breaks
$tmp = preg_replace('/\n/', '<span class="text-muted user-select-none">\\n</span><br>', $tmp);
$tmp = preg_replace('/\r/', '<span class="text-muted user-select-none">\\r</span>', $tmp);
$tmp = preg_replace('/\t/', '<span class="text-muted user-select-none">\\t</span>', $tmp);
return $tmp;
}
if (is_bool($data)) {
return $this->formatBool($data);
}
if (is_int($data)) {
return (string) $data;
}
if (is_float($data)) {
return (string) $data;
}
if (is_null($data)) {
return '<i>null</i>';
}
if (is_array($data)) {
//If the array contains only one element with the key @id, it is a reference to another entity (foreign key)
if (isset($data['@id'])) {
return $this->formatForeignKey($data, $logEntry, $fieldName);
}
//If the array contains a "date", "timezone_type" and "timezone" key, it is a DateTime object
if (isset($data['date'], $data['timezone_type'], $data['timezone'])) {
return $this->formatDateTime($data);
}
return $this->formatJSON($data);
}
throw new \RuntimeException('Type of $data not supported (' . gettype($data) . ')');
}
private function formatJSON(array $data): string
{
$json = htmlspecialchars(json_encode($data, JSON_PRETTY_PRINT), ENT_QUOTES | ENT_SUBSTITUTE);
return sprintf(
'<div data-controller="elements--json-formatter" data-json="%s"></div>',
$json
);
}
private function formatForeignKey(array $data, AbstractLogEntry $logEntry, string $fieldName): string
{
//Extract the id from the @id key
$id = $data['@id'];
try {
//Retrieve the class type from the logEntry and retrieve the doctrine metadata
$classMetadata = $this->entityManager->getClassMetadata($logEntry->getTargetClass());
$fkTargetClass = $classMetadata->getAssociationTargetClass($fieldName);
//Try to retrieve the entity from the database
$entity = $this->entityManager->getRepository($fkTargetClass)->find($id);
//If the entity was found, return a label for this entity
if ($entity) {
return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($entity, true);
} else { //Otherwise the entity was deleted, so return the id
return $this->elementTypeNameGenerator->formatElementDeletedHTML($fkTargetClass, $id);
}
} catch (\InvalidArgumentException|\ReflectionException $exception) {
return '<i>unknown target class</i>: ' . $id;
}
}
private function formatDateTime(array $data): string
{
if (!isset($data['date'], $data['timezone_type'], $data['timezone'])) {
return '<i>unknown DateTime format</i>';
}
$date = $data['date'];
$timezoneType = $data['timezone_type'];
$timezone = $data['timezone'];
if (!is_string($date) || !is_int($timezoneType) || !is_string($timezone)) {
return '<i>unknown DateTime format</i>';
}
try {
$dateTime = new \DateTime($date, new \DateTimeZone($timezone));
} catch (\Exception $exception) {
return '<i>unknown DateTime format</i>';
}
//Format it to the users locale
$formatter = new \IntlDateFormatter(null, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM);
return $formatter->format($dateTime);
}
private function formatBool(bool $data): string
{
return $data ? $this->translator->trans('true') : $this->translator->trans('false');
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LogSystem;
use Jfcherng\Diff\DiffHelper;
class LogDiffFormatter
{
/**
* Format the diff between the given data, depending on the type of the data.
* If the diff is not possible, an empty string is returned.
* @param $old_data
* @param $new_data
* @return string
*/
public function formatDiff($old_data, $new_data): string
{
if (is_string($old_data) && is_string($new_data)) {
return $this->diffString($old_data, $new_data);
}
if (is_numeric($old_data) && is_numeric($new_data)) {
return $this->diffNumeric($old_data, $new_data);
}
return '';
}
private function diffString(string $old_data, string $new_data): string
{
return DiffHelper::calculate($old_data, $new_data, 'Combined',
[ //Diff options
'context' => 2,
],
[ //Render options
'detailLevel' => 'char',
'showHeader' => false,
]);
}
private function diffNumeric($old_data, $new_data): string
{
if ((!is_numeric($old_data)) || (!is_numeric($new_data))) {
throw new \InvalidArgumentException('The given data is not numeric.');
}
$difference = $new_data - $old_data;
//Positive difference
if ($difference > 0) {
return sprintf('<span class="text-success">+%s</span>', $difference);
} else if ($difference < 0) {
return sprintf('<span class="text-danger">%s</span>', $difference);
} else {
return sprintf('<span class="text-muted">%s</span>', $difference);
}
}
}

View File

@@ -131,9 +131,9 @@ class LogEntryExtraFormatter
if (($context instanceof LogWithEventUndoInterface) && $context->isUndoEvent()) {
if ('undo' === $context->getUndoMode()) {
$array['log.undo_mode.undo'] = (string) $context->getUndoEventID();
$array['log.undo_mode.undo'] = '#' . $context->getUndoEventID();
} elseif ('revert' === $context->getUndoMode()) {
$array['log.undo_mode.revert'] = (string) $context->getUndoEventID();
$array['log.undo_mode.revert'] = '#' . $context->getUndoEventID();
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LogSystem;
use App\Entity\LogSystem\AbstractLogEntry;
use Psr\Log\LogLevel;
class LogLevelHelper
{
/**
* Returns the FontAwesome icon class for the given log level.
* This returns just the specific icon class (so 'fa-info' for example).
* @param string $logLevel The string representation of the log level (one of the LogLevel::* constants)
* @return string
*/
public function logLevelToIconClass(string $logLevel): string
{
switch ($logLevel) {
case LogLevel::DEBUG:
return 'fa-bug';
case LogLevel::INFO:
return 'fa-info';
case LogLevel::NOTICE:
return 'fa-flag';
case LogLevel::WARNING:
return 'fa-exclamation-circle';
case LogLevel::ERROR:
return 'fa-exclamation-triangle';
case LogLevel::CRITICAL:
return 'fa-bolt';
case LogLevel::ALERT:
return 'fa-radiation';
case LogLevel::EMERGENCY:
return 'fa-skull-crossbones';
default:
return 'fa-question-circle';
}
}
/**
* Returns the Bootstrap table color class for the given log level.
* @param string $logLevel The string representation of the log level (one of the LogLevel::* constants)
* @return string The table color class (one of the 'table-*' classes)
*/
public function logLevelToTableColorClass(string $logLevel): string
{
switch ($logLevel) {
case LogLevel::EMERGENCY:
case LogLevel::ALERT:
case LogLevel::CRITICAL:
case LogLevel::ERROR:
return 'table-danger';
case LogLevel::WARNING:
return 'table-warning';
case LogLevel::NOTICE:
return 'table-info';
default:
return '';
}
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LogSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Entity\LogSystem\UserNotAllowedLogEntry;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\PartLot;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Exceptions\EntityNotSupportedException;
use App\Repository\LogEntryRepository;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class LogTargetHelper
{
protected EntityManagerInterface $em;
protected LogEntryRepository $entryRepository;
protected EntityURLGenerator $entityURLGenerator;
protected ElementTypeNameGenerator $elementTypeNameGenerator;
protected TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, EntityURLGenerator $entityURLGenerator,
ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator)
{
$this->em = $entityManager;
$this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class);
$this->entityURLGenerator = $entityURLGenerator;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->translator = $translator;
}
private function configureOptions(OptionsResolver $resolver): self
{
$resolver->setDefault('show_associated', true);
$resolver->setDefault('showAccessDeniedPath', true);
return $this;
}
public function formatTarget(AbstractLogEntry $context, array $options = []): string
{
$optionsResolver = new OptionsResolver();
$this->configureOptions($optionsResolver);
$options = $optionsResolver->resolve($options);
if ($context instanceof UserNotAllowedLogEntry && $options['showAccessDeniedPath']) {
return htmlspecialchars($context->getPath());
}
/** @var AbstractLogEntry $context */
$target = $this->entryRepository->getTargetElement($context);
//If the target is null and the context has a target, that means that the target was deleted. Show it that way.
if ($target === null) {
if ($context->hasTarget()) {
return $this->elementTypeNameGenerator->formatElementDeletedHTML($context->getTargetClass(),
$context->getTargetId());
}
//If no target is set, we can't do anything
return '';
}
//Otherwise we can return a label for the target
return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($target, $options['show_associated']);
}
}

View File

@@ -194,7 +194,7 @@ class TimeTravel
public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logEntry): void
{
//Skip if this does not provide any info...
if (!$logEntry->hasOldDataInformations()) {
if (!$logEntry->hasOldDataInformation()) {
return;
}
if (!$element instanceof TimeStampableInterface) {

View File

@@ -71,6 +71,8 @@ final class EntityExtension extends AbstractExtension
new TwigFunction('entity_type', [$this, 'getEntityType']),
/* Returns the URL to the given entity */
new TwigFunction('entity_url', [$this, 'generateEntityURL']),
/* Returns the URL to the given entity in timetravel mode */
new TwigFunction('timetravel_url', [$this->entityURLGenerator, 'timetravelURL']),
/* Generates a JSON array of the given tree */
new TwigFunction('tree_data', [$this, 'treeData']),

47
src/Twig/LogExtension.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Twig;
use App\Services\LogSystem\LogDataFormatter;
use App\Services\LogSystem\LogDiffFormatter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class LogExtension extends AbstractExtension
{
private LogDataFormatter $logDataFormatter;
private LogDiffFormatter $logDiffFormatter;
public function __construct(LogDataFormatter $logDataFormatter, LogDiffFormatter $logDiffFormatter)
{
$this->logDataFormatter = $logDataFormatter;
$this->logDiffFormatter = $logDiffFormatter;
}
public function getFunctions()
{
return [
new TwigFunction('format_log_data', [$this->logDataFormatter, 'formatData'], ['is_safe' => ['html']]),
new TwigFunction('format_log_diff', [$this->logDiffFormatter, 'formatDiff'], ['is_safe' => ['html']]),
];
}
}

View File

@@ -0,0 +1,15 @@
{# @var entry \App\Entity\LogSystem\CollectionElementDeleted #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
<p class="m-0">
<b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>:
{{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
{% if entry.oldName is not empty %}
({{ entry.oldName }})
{% endif %}
</p>
<p class="m-0">
<b>{% trans %}log.collection_deleted.on_collection{% endtrans %}</b>:
{{ log_helper.translate_field(entry.collectionName) }}
</p>

View File

@@ -0,0 +1,23 @@
{# @var entry \App\Entity\LogSystem\DatabaseUpdatedLogEntry #}
{% if entry.successful %}
<h5><span class="badge bg-success badge-success">
<i class="fa-solid fa-check"></i>
{% trans %}log.database_updated.success{% endtrans %}
</span></h5>
{% else %}
<h5><span class="badge bg-danger badge-danger">
<i class="fa-solid fa-xmark"></i>
{% trans %}log.database_updated.failed{% endtrans %}</span>
</h5>
{% endif %}
<span class="badge bg-secondary badge-secondary badge-pill" title="{% trans %}log.database_updated.old_version{% endtrans %}">
<i class="fa-solid fa-database"></i>
{{ entry.oldVersion }}
</span>
<i class="fa-solid fa-arrow-right-long"></i>
<span class="badge bg-primary badge-primary badge-pill" title="{% trans %}log.database_updated.new_version{% endtrans %}">
<i class="fa-solid fa-database"></i>
{{ entry.newVersion }}
</span>

View File

@@ -0,0 +1,11 @@
{# @var entry \App\Entity\LogSystem\ElementCreatedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
{{ log_helper.comment_field(entry) }}
{% if entry.creationInstockValue %}
<p>
<b>{% trans %}log.element_created.original_instock{% endtrans %}:</b>
{{ entry.creationInstockValue }}
</p>
{% endif %}

View File

@@ -0,0 +1,8 @@
{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
<span class="badge badge-notice"></span>
{{ log_helper.comment_field(entry) }}
{{ log_helper.data_change_table(entry) }}

View File

@@ -0,0 +1,11 @@
{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
{% if entry.undoEvent %}
<span class="badge badge-info bg-info">Test</span>
{% endif %}
{{ log_helper.comment_field(entry) }}
{{ log_helper.data_change_table(entry) }}

View File

@@ -0,0 +1,9 @@
{# @var entry \App\Entity\LogSystem\UserLoginLogEntry #}
IP: &nbsp;
<span class="badge bg-primary badge-primary">
<i class="fa-solid fa-network-wired"></i>
{{ entry.iPAddress }}
</span>
<p class="text-muted">{% trans %}log.user_login.ip_anonymize_hint{% endtrans %}</p>

View File

@@ -0,0 +1,9 @@
{# @var entry \App\Entity\LogSystem\UserLoginLogEntry #}
{% trans %}log.user_login.login_from_ip{% endtrans %}: &nbsp;
<span class="badge bg-primary badge-primary">
<i class="fa-solid fa-network-wired"></i>
{{ entry.iPAddress }}
</span>
<p class="text-muted">{% trans %}log.user_login.ip_anonymize_hint{% endtrans %}</p>

View File

@@ -0,0 +1,4 @@
{# @var entry \App\Entity\LogSystem\UserNotAllowedLogEntry #}
{% trans %}log.user_not_allowed.unauthorized_access_attempt_to{% endtrans %}: &nbsp;<code>{{ entry.path }}</code>
<p class="text-muted">{% trans %}log.user_not_allowed.hint{% endtrans %}</p>

View File

@@ -0,0 +1,143 @@
{% macro undo_buttons(entry, target_element) %}
{# @var entry \App\Entity\LogSystem\ElementEditedLogEntry|\App\Entity\LogSystem\ElementDeletedLogEntry entry #}
{% set disabled = not is_granted('revert_element', entry.targetClass) %}
{% if entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted')
or (entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') and entry.hasOldDataInformation) %}
{% set icon = 'fa-trash-restore' %}
{% set title = 'log.undo.undelete'|trans %}
{% set title_short = 'log.undo.undelete.short'|trans %}
{% elseif entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry')
or (entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') and entry.hasOldDataInformation) %}
{% set icon = 'fa-undo' %}
{% set title = 'log.undo.undo'|trans %}
{% set title_short = 'log.undo.undo.short'|trans %}
{% endif %}
<form method="post" action="{{ path("log_undo") }}"
{{ stimulus_controller('elements/delete_btn') }} {{ stimulus_action('elements/delete_btn', "submit", "submit") }}
data-delete-title="{% trans %}log.undo.confirm_title{% endtrans %}"
data-delete-message="{% trans %}log.undo.confirm_message{% endtrans %}">
<input type="hidden" name="redirect_back" value="{{ app.request.requestUri }}">
<div class="btn-group btn-group-sm" role="group">
<button type="submit" class="btn btn-outline-secondary" name="undo" value="{{ entry.id }}" {% if disabled %}disabled{% endif %}>
<i class="fas fa-fw {{ icon }}" title="{{ title }}"></i> {{ title_short }}
</button>
<button type="submit" class="btn btn-outline-secondary" name="revert" value="{{ entry.id }}" {% if disabled %}disabled{% endif %}>
<i class="fas fa-fw fa-backward" title="{% trans %}log.undo.revert{% endtrans %}"></i> {{ 'log.undo.revert.short' | trans }}
</button>
{# View button #}
{% if target_element and ((attribute(entry, 'oldDataInformation') is defined and entry.oldDataInformation)
or entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted')) %}
<a class="btn btn-outline-secondary" href="{{ timetravel_url(target_element, entry.timestamp)}}"><i class="fas fa-fw fa-eye"></i>
{% trans %}log.view_version{% endtrans %}
</a>
{% endif %}
</div>
</form>
{% endmacro %}
{% macro comment_field(entry) %}
{# @var entry \App\Entity\Contracts\LogWithComment #}
<p class="mb-0">
<b>{% trans %}edit.log_comment{% endtrans %}:</b>
{% if entry.comment %}
{{ entry.comment }}
{% else %}
<span class="text-muted">{% trans %}log.no_comment{% endtrans %}</span>
{% endif %}
</p>
{% endmacro %}
{% macro translate_field(field) %}
{% set trans_key = 'log.element_edited.changed_fields.'~field %}
{# If the translation key is not found, the translation key is returned, and we dont show the translation #}
{% if trans_key|trans != trans_key %}
{{ ('log.element_edited.changed_fields.'~field) | trans }}
<span class="text-muted">({{ field }})</span>
{% else %}
{{ field }}
{% endif %}
{% endmacro %}
{% macro data_change_table(entry) %}
{# @var entry \App\Entity\LogSystem\ElementEditedLogEntry|\App\Entity\LogSystem\ElementDeletedLogEntry entry #}
{% set fields, old_data, new_data = {}, {}, {} %}
{# For log entries where only the changed fields are saved, this is the last executed assignment #}
{% if attribute(entry, 'changedFieldInfo') is defined and entry.changedFieldsInfo %}
{% set fields = entry.changedFields %}
{% endif %}
{# For log entries, where we know the old data, this is the last exectuted assignment #}
{% if attribute(entry, 'oldDataInformation') is defined and entry.oldDataInformation %}
{# We have to use the keys of oldData here, as changedFields might not be available #}
{% set fields = entry.oldData | keys %}
{% set old_data = entry.oldData %}
{% endif %}
{# For log entries, where we have new data, we define it #}
{% if attribute(entry, 'newDataInformation') is defined and entry.newDataInformation %}
{# We have to use the keys of oldData here, as changedFields might not be available #}
{% set fields = entry.newData | keys %}
{% set new_data = entry.newData %}
{% endif %}
{% if fields is not empty %}
<table class="table table-hover table-striped table-sm table-bordered mt-2">
<thead>
<tr>
<th>{% trans %}log.element_changed.field{% endtrans %}</th>
{% if old_data is not empty %}
<th>{% trans %}log.element_changed.data_before{% endtrans %}</th>
{% endif %}
{% if new_data is not empty %}
<th>{% trans %}log.element_changed.data_after{% endtrans %}</th>
{% endif %}
{% if new_data is not empty and old_data is not empty %} {# Diff column #}
<th>{% trans %}log.element_changed.diff{% endtrans %}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for field in fields %}
<tr>
<td title="{{ field }}">
{{ _self.translate_field(field) }}
</td>
{% if old_data is not empty %}
<td>
{% if old_data[field] is defined %}
{{ format_log_data(old_data[field], entry, field) }}
{% endif %}
</td>
{% endif %}
{% if new_data is not empty %}
<td>
{% if new_data[field] is defined %}
{{ format_log_data(new_data[field], entry, field) }}
{% endif %}
</td>
{% endif %}
{% if new_data is not empty and old_data is not empty %}
<td>
{% if new_data[field] is defined and old_data[field] is defined %}
{{ format_log_diff(old_data[field], new_data[field]) }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,117 @@
{% extends "main_card.html.twig" %}
{% import "helper.twig" as helper %}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
{% block title %}
{% trans %}log.details.title{% endtrans %}:
{{ ('log.type.' ~ log_entry.type) | trans }} ({{ log_entry.timestamp | format_datetime('short') }})
{% endblock %}
{% block card_title %}
<i class="fas fa-binoculars"></i>
{% trans %}log.details.title{% endtrans %}:
<i>{{ ('log.type.' ~ log_entry.type) | trans }}</i> ({{ log_entry.timestamp | format_datetime('short') }})
<span class="float-end">ID: {{ log_entry.iD }}</span>
{% endblock %}
{% block card_body %}
<table class="table table-striped table-hover mb-0 {{ log_level_helper.logLevelToTableColorClass(log_entry.levelString) }}">
<tr>
<td>{% trans %}log.timestamp{% endtrans %}</td>
<td>{{ log_entry.timestamp | format_datetime('full') }}</td>
</tr>
<tr>
<td>{% trans %}log.type{% endtrans %}</td>
<td>
{{ ('log.type.' ~ log_entry.type) | trans }}
{% if log_entry.type == 'part_stock_changed' %}
({{ ('log.part_stock_changed.' ~ log_entry.instockChangeType)|trans }})
{% endif %}
{% if log_entry is instanceof('App\\Entity\\Contracts\\LogWithEventUndoInterface') and log_entry.undoEvent %}
<b>({{ ('log.undo_mode.' ~ log_entry.undoMode)|trans }}: <a href="{{ path('log_details', {"id": log_entry.UndoEventID}) }}">#{{ log_entry.UndoEventID }}</a>)</b>
{% endif %}
</td>
</tr>
<tr>
<td>{% trans %}log.level{% endtrans %}</td>
<td>
<i class="fa-solid {{ log_level_helper.logLevelToIconClass(log_entry.levelString) }} fa-fw"></i>
{{ ('log.level.'~ log_entry.levelString)|trans }}
</td>
</tr>
<tr>
<td>{% trans %}log.user{% endtrans %}
<td>
{% if log_entry.cLIEntry %}
<i class="fa-solid fa-terminal"></i>
{{ log_entry.cLIUsername }} ({% trans %}log.cli_user{% endtrans %})
{% else %}
{% if log_entry.user %}
{{ helper.user_icon_link(log_entry.user) }} (@{{ log_entry.user.username }})
{% else %}
@{{ log_entry.username }} ({% trans %}log.target_deleted{% endtrans %}
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<td>{% trans %}log.target{% endtrans %}</td>
<td>{{ target_html|raw }}</td>
</tr>
</table>
<div class="card-body">
<div class="row mb-2">
<div class="col-6">
{% if log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted')
or log_entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry')
or log_entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry')
or log_entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry')
%}
{{ log_helper.undo_buttons(log_entry, target_element) }}
{% endif %}
</div>
<div class="col-6 text-end">
<form method="post" class="" action="{{ path('log_delete', {'id': log_entry.iD}) }}" {{ stimulus_controller('elements/delete_btn') }} {{ stimulus_action('elements/delete_btn', "submit", "submit") }}
data-delete-title="{% trans %}log.delete.message.title{% endtrans %}"
data-delete-message="{% trans %}log.delete.message{% endtrans %}">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ log_entry.id) }}">
<button type="submit" class="btn btn-sm btn-outline-danger" {% if not is_granted('delete', log_entry) %}disabled{% endif %}>
<i class="fa-solid fa-trash"></i>
{% trans %}log.details.delete_entry{% endtrans %}
</button>
</form>
</div>
</div>
{# This assignment is to improve autocomplete on the subpages, as PHPstorm ignores typehints for log_entry #}
{% set entry = log_entry %}
{% if log_entry is instanceof('App\\Entity\\LogSystem\\DatabaseUpdatedLogEntry') %}
{% include "log_system/details/_extra_database_updated.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry') %}
{% include "log_system/details/_extra_element_created.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') %}
{% include "log_system/details/_extra_element_edited.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') %}
{% include "log_system/details/_extra_element_deleted.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserLoginLogEntry')
or log_entry is instanceof('App\\Entity\\LogSystem\\UserLogoutLogEntry') %}
{% include "log_system/details/_extra_user_login.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserNotAllowedLogEntry') %}
{% include "log_system/details/_extra_user_not_allowed.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\SecurityEventLogEntry') %}
{% include "log_system/details/_extra_security_event.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %}
{% include "log_system/details/_extra_collection_element_deleted.html.twig" %}
{% else %}
{{ extra_html | raw }}
{% endif %}
</div>
{% endblock %}

View File

@@ -4654,7 +4654,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
</notes>
<segment state="translated">
<source>log.undo.undelete</source>
<target>Bauteil wiederherstellen</target>
<target>Element wiederherstellen</target>
</segment>
</unit>
<unit id="PI8faHR" name="log.undo.undo">
@@ -11255,5 +11255,179 @@ Element 3</target>
<target>Weniger als erwünscht</target>
</segment>
</unit>
<unit id="cdnsW4q" name="log.cli_user">
<segment state="translated">
<source>log.cli_user</source>
<target>CLI Benutzer</target>
</segment>
</unit>
<unit id="4GTAJ9E" name="log.element_edited.changed_fields.part_owner_must_match">
<segment state="translated">
<source>log.element_edited.changed_fields.part_owner_must_match</source>
<target>Der Bauteilebesitzer muss dem Lagerortbesitzer entsprechen!</target>
</segment>
</unit>
<unit id="u6qFa_j" name="part.filter.lessThanDesired">
<segment state="translated">
<source>part.filter.lessThanDesired</source>
<target>Weniger vorhanden als gewünscht (Gesamtmenge &lt; Mindestmenge)</target>
</segment>
</unit>
<unit id="lHTN.a1" name="part.filter.lotOwner">
<segment state="translated">
<source>part.filter.lotOwner</source>
<target>Besitzer des Bestands</target>
</segment>
</unit>
<unit id="47OCK_W" name="user.show_email_on_profile.label">
<segment state="translated">
<source>user.show_email_on_profile.label</source>
<target>E-Mail-Adresse auf öffentlicher Profilseite anzeigen</target>
</segment>
</unit>
<unit id="4rkjIk2" name="log.details.title">
<segment state="translated">
<source>log.details.title</source>
<target>Logdetails</target>
</segment>
</unit>
<unit id="aeYMkHS" name="log.user_login.login_from_ip">
<segment state="translated">
<source>log.user_login.login_from_ip</source>
<target>Login von IP-Adresse</target>
</segment>
</unit>
<unit id="9jOklgS" name="log.user_login.ip_anonymize_hint">
<segment state="translated">
<source>log.user_login.ip_anonymize_hint</source>
<target>Wenn die letzten Stellen der IP-Adresse fehlen, dann ist der DSGV Modus aktiviert, bei dem IP-Adressen anonymisiert werden.</target>
</segment>
</unit>
<unit id="kaMyDVi" name="log.user_not_allowed.unauthorized_access_attempt_to">
<segment state="translated">
<source>log.user_not_allowed.unauthorized_access_attempt_to</source>
<target>Unerlaubter Zugriffsversuch auf Seite</target>
</segment>
</unit>
<unit id="EibB1Wh" name="log.user_not_allowed.hint">
<segment state="translated">
<source>log.user_not_allowed.hint</source>
<target>Die Anfrage wurde blockiert. Es sollte keine weitere Aktion nötig sein.</target>
</segment>
</unit>
<unit id="JVE17kW" name="log.no_comment">
<segment state="translated">
<source>log.no_comment</source>
<target>Kein Kommentar</target>
</segment>
</unit>
<unit id="5xvPvME" name="log.element_changed.field">
<segment state="translated">
<source>log.element_changed.field</source>
<target>Feld</target>
</segment>
</unit>
<unit id="vufWYhV" name="log.element_changed.data_before">
<segment state="translated">
<source>log.element_changed.data_before</source>
<target>Daten vor Änderung</target>
</segment>
</unit>
<unit id="qk2u9Qg" name="error_table.error">
<segment state="translated">
<source>error_table.error</source>
<target>Während der Anfrage trat ein Fehler auf.</target>
</segment>
</unit>
<unit id="tLXzED2" name="part.table.invalid_regex">
<segment state="translated">
<source>part.table.invalid_regex</source>
<target>Ungültiger regulärer Ausdruck (regex)</target>
</segment>
</unit>
<unit id="VKPOkNO" name="log.element_changed.data_after">
<segment state="translated">
<source>log.element_changed.data_after</source>
<target>Daten nach Änderung</target>
</segment>
</unit>
<unit id="DiNGTl8" name="log.element_changed.diff">
<segment state="translated">
<source>log.element_changed.diff</source>
<target>Unterschied</target>
</segment>
</unit>
<unit id="OB_fVDI" name="log.undo.undo.short">
<segment state="translated">
<source>log.undo.undo.short</source>
<target>Rückgängig machen</target>
</segment>
</unit>
<unit id="AvoT6DL" name="log.undo.revert.short">
<segment state="translated">
<source>log.undo.revert.short</source>
<target>Auf Version zurücksetzen</target>
</segment>
</unit>
<unit id="YdXQd2_" name="log.view_version">
<segment state="translated">
<source>log.view_version</source>
<target>Version anzeigen</target>
</segment>
</unit>
<unit id="l47W4kt" name="log.undo.undelete.short">
<segment state="translated">
<source>log.undo.undelete.short</source>
<target>Wiederherstellen</target>
</segment>
</unit>
<unit id="PDJYeqj" name="log.element_edited.changed_fields.id">
<segment state="translated">
<source>log.element_edited.changed_fields.id</source>
<target>ID</target>
</segment>
</unit>
<unit id="cQTNNI7" name="log.element_edited.changed_fields.id_owner">
<segment state="translated">
<source>log.element_edited.changed_fields.id_owner</source>
<target>Besitzer</target>
</segment>
</unit>
<unit id="h1eBlp8" name="log.element_edited.changed_fields.parent_id">
<segment state="translated">
<source>log.element_edited.changed_fields.parent_id</source>
<target>Übergeordnetes Element</target>
</segment>
</unit>
<unit id="GUthgcU" name="log.details.delete_entry">
<segment state="translated">
<source>log.details.delete_entry</source>
<target>Logeintrag löschen</target>
</segment>
</unit>
<unit id="Wv0WAmO" name="log.delete.message.title">
<segment state="translated">
<source>log.delete.message.title</source>
<target>Wollen Sie diesen Logeintrag wirklich löschen?</target>
</segment>
</unit>
<unit id="5tbpaLR" name="log.delete.message">
<segment state="translated">
<source>log.delete.message</source>
<target>Wenn dies ein Historieeintrag für ein Element ist, dann wird das Löschen zu Datenverlust der Historie führen! Dies kann unerwartete Ergebnisse liefern, wenn die Zeitreisefunktion verwendet wird.</target>
</segment>
</unit>
<unit id="P.c4WmY" name="log.collection_deleted.on_collection">
<segment state="translated">
<source>log.collection_deleted.on_collection</source>
<target>in Kollektion</target>
</segment>
</unit>
<unit id=".wiLJk6" name="log.element_edited.changed_fields.attachments">
<segment state="translated">
<source>log.element_edited.changed_fields.attachments</source>
<target>Dateianhänge</target>
</segment>
</unit>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled">
<segment>
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>Your account is disabled! Contact an administrator if you think this is wrong.</target>
</segment>

View File

@@ -37,7 +37,7 @@
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>The preview attachment must be a valid picture!</target>
</segment>
@@ -82,7 +82,7 @@
<note priority="1">src\Entity\StructuralDBElement.php:0</note>
<note priority="1">src\Entity\Supplier.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>An element with this name already exists on this level!</target>
</segment>
@@ -102,7 +102,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target>
</segment>
@@ -122,7 +122,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>Value must be lesser than the maximum value ({{ compared_value }}).</target>
</segment>
@@ -142,7 +142,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>Value must be greater or equal than the typical value ({{ compared_value }}).</target>
</segment>
@@ -152,7 +152,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>validator.user.username_already_used</source>
<target>A user with this name is already exisiting</target>
</segment>
@@ -162,7 +162,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>user.invalid_username</source>
<target>The username must contain only letters, numbers, underscores, dots, pluses or minuses!</target>
</segment>
@@ -171,7 +171,7 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>An element can not be its own parent!</target>
</segment>
@@ -180,121 +180,121 @@
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>You can not assign children element as parent (This would cause loops)!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<segment>
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Please select a valid category!</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<segment>
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Can not add new parts to this location as it is marked as "Only Existing"</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<segment>
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment>
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Location is full. Can not add new parts to it.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment>
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>This location can only contain a single part and it is already full!</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<segment>
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>You must select an attachment type!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<segment>
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>You must select an supplier!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment>
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>To enable SI prefixes, you have to set a unit symbol!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<segment>
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>The internal part number must be unique. {{ value }} is already in use!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<segment>
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>There is already an BOM entry with this name!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<segment>
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>This part already exists in the BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<segment>
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>The number of mountnames has to match the BOMs quantity!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<segment>
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>You can not add a project's own builds part to the BOM.</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<segment>
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<segment>
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<segment>
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<segment>
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>The part name does not match the regular expression stated by the category: %regex%</target>
</segment>

1752
yarn.lock

File diff suppressed because it is too large Load Diff