From fce32e70b9eaba60c8681f0ad804c357fcc98634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 23 Mar 2023 01:16:12 +0100 Subject: [PATCH 01/12] Started to work on an import possibility for Partkeepr databases --- composer.json | 1 + .../Migrations/ImportPartKeeprCommand.php | 96 + .../Purger/ResetAutoIncrementORMPurger.php | 2 +- .../ImportExportSystem/PartkeeprImporter.php | 154 + src/Services/Misc/MySQLDumpXMLConverter.php | 107 + .../Misc/MySQLDumpXMLConverterTest.php | 54 + tests/assets/partkeepr_import_test.xml | 8952 +++++++++++++++++ 7 files changed, 9365 insertions(+), 1 deletion(-) create mode 100644 src/Command/Migrations/ImportPartKeeprCommand.php create mode 100644 src/Services/ImportExportSystem/PartkeeprImporter.php create mode 100644 src/Services/Misc/MySQLDumpXMLConverter.php create mode 100644 tests/Services/Misc/MySQLDumpXMLConverterTest.php create mode 100644 tests/assets/partkeepr_import_test.xml diff --git a/composer.json b/composer.json index c823221d..af991b44 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "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", diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php new file mode 100644 index 00000000..d860ce03 --- /dev/null +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -0,0 +1,96 @@ +. + */ + +namespace App\Command\Migrations; + +use App\Services\ImportExportSystem\PartkeeprImporter; +use App\Services\Misc\MySQLDumpXMLConverter; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ImportPartKeeprCommand extends Command +{ + + protected static $defaultName = 'partdb:import-partkeepr'; + + protected EntityManagerInterface $em; + protected PartkeeprImporter $importer; + protected MySQLDumpXMLConverter $xml_converter; + + public function __construct(EntityManagerInterface $em, PartkeeprImporter $importer, MySQLDumpXMLConverter $xml_converter) + { + parent::__construct(self::$defaultName); + $this->em = $em; + $this->importer = $importer; + $this->xml_converter = $xml_converter; + } + + protected function configure() + { + $this->setDescription('Import a PartKeepr database dump into Part-DB'); + + $this->addArgument('file', InputArgument::REQUIRED, 'The file to which should be imported.'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $input_path = $input->getArgument('file'); + + //Make more checks here + //$io->confirm('This will delete all data in the database. Do you want to continue?', false); + + //Purge the databse, so we will not have any conflicts + $this->importer->purgeDatabaseForImport(); + + //Convert the XML file to an array + $xml = file_get_contents($input_path); + $data = $this->xml_converter->convertMySQLDumpXMLDataToArrayStructure($xml); + + //Import the data + $this->doImport($io, $data); + + return 0; + } + + private function doImport(SymfonyStyle $io, array $data) + { + //First import the distributors + $io->info('Importing distributors...'); + $count = $this->importer->importDistributors($data); + $io->success('Imported '.$count.' distributors.'); + + //Import the measurement units + $io->info('Importing part measurement units...'); + $count = $this->importer->importPartUnits($data); + $io->success('Imported '.$count.' measurement units.'); + + //Import manufacturers + $io->info('Importing manufacturers...'); + $count = $this->importer->importManufacturers($data); + $io->success('Imported '.$count.' manufacturers.'); + } + +} \ No newline at end of file diff --git a/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php index a38045d4..811facf6 100644 --- a/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php +++ b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php @@ -179,7 +179,7 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface } // If the table is excluded, skip it as well - if (array_search($tbl, $this->excluded) !== false) { + if (in_array($tbl, $this->excluded, true)) { continue; } diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php new file mode 100644 index 00000000..d2bf13f7 --- /dev/null +++ b/src/Services/ImportExportSystem/PartkeeprImporter.php @@ -0,0 +1,154 @@ +. + */ + +namespace App\Services\ImportExportSystem; + +use App\Doctrine\Purger\ResetAutoIncrementORMPurger; +use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\Supplier; +use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory; +use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +class PartkeeprImporter +{ + + protected EntityManagerInterface $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + public function purgeDatabaseForImport(): void + { + //Versions with "" are needed !! + $purger = new ResetAutoIncrementORMPurger($this->em, ['users', '"users"', 'groups', '"groups"', 'u2f_keys', 'internal', 'migration_versions']); + $purger->purge(); + } + + /** + * Imports the distributors from the given data. + * @param array $data The data to import (associated array, containing a 'distributor' key + * @return int The number of imported distributors + */ + public function importDistributors(array $data): int + { + if (!isset($data['distributor'])) { + throw new \RuntimeException('$data must contain a "distributor" key!'); + } + + $distributor_data = $data['distributor']; + + foreach ($distributor_data as $distributor) { + $supplier = new Supplier(); + $supplier->setName($distributor['name']); + $supplier->setWebsite($distributor['url'] ?? ''); + $supplier->setAddress($distributor['address'] ?? ''); + $supplier->setPhoneNumber($distributor['phone'] ?? ''); + $supplier->setFaxNumber($distributor['fax'] ?? ''); + $supplier->setEmailAddress($distributor['email'] ?? ''); + $supplier->setComment($distributor['comment']); + $supplier->setAutoProductUrl($distributor['skuurl'] ?? ''); + + $this->setIDOfEntity($supplier, $distributor['id']); + $this->em->persist($supplier); + } + + $this->em->flush(); + + return count($distributor_data); + } + + public function importManufacturers(array $data): int + { + if (!isset($data['manufacturer'])) { + throw new \RuntimeException('$data must contain a "manufacturer" key!'); + } + + $manufacturer_data = $data['manufacturer']; + + $max_id = 0; + + //Assign a parent manufacturer to all manufacturers, as partkeepr has a lot of manufacturers by default + $parent_manufacturer = new Manufacturer(); + $parent_manufacturer->setName('PartKeepr'); + $parent_manufacturer->setNotSelectable(true); + + foreach ($manufacturer_data as $manufacturer) { + $entity = new Manufacturer(); + $entity->setName($manufacturer['name']); + $entity->setWebsite($manufacturer['url'] ?? ''); + $entity->setAddress($manufacturer['address'] ?? ''); + $entity->setPhoneNumber($manufacturer['phone'] ?? ''); + $entity->setFaxNumber($manufacturer['fax'] ?? ''); + $entity->setEmailAddress($manufacturer['email'] ?? ''); + $entity->setComment($manufacturer['comment']); + $entity->setParent($parent_manufacturer); + + $this->setIDOfEntity($entity, $manufacturer['id']); + $this->em->persist($entity); + + $max_id = max($max_id, $manufacturer['id']); + } + + //Set the ID of the parent manufacturer to the max ID + 1, to avoid trouble with the auto increment + $this->setIDOfEntity($parent_manufacturer, $max_id + 1); + $this->em->persist($parent_manufacturer); + + $this->em->flush(); + + return count($manufacturer_data); + } + + public function importPartUnits(array $data): int + { + if (!isset($data['partunit'])) { + throw new \RuntimeException('$data must contain a "partunit" key!'); + } + + $partunit_data = $data['partunit']; + foreach ($partunit_data as $partunit) { + $unit = new MeasurementUnit(); + $unit->setName($partunit['name']); + $unit->setUnit($partunit['shortName'] ?? null); + + $this->setIDOfEntity($unit, $partunit['id']); + $this->em->persist($unit); + } + + $this->em->flush(); + + return count($partunit_data); + } + + public function setIDOfEntity(AbstractDBElement $element, int $id): void + { + $metadata = $this->em->getClassMetadata(get_class($element)); + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); + $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); + $metadata->setIdentifierValues($element, ['id' => $id]); + } +} \ No newline at end of file diff --git a/src/Services/Misc/MySQLDumpXMLConverter.php b/src/Services/Misc/MySQLDumpXMLConverter.php new file mode 100644 index 00000000..9f9a01fc --- /dev/null +++ b/src/Services/Misc/MySQLDumpXMLConverter.php @@ -0,0 +1,107 @@ +. + */ + +namespace App\Services\Misc; + +class MySQLDumpXMLConverter +{ + + /** + * Converts a MySQL dump XML file to an associative array structure in the following form + * [ + * 'table_name' => [ + * [ + * 'column_name' => 'value', + * 'column_name' => 'value', + * ... + * ], + * [ + * 'column_name' => 'value', + * 'column_name' => 'value', + * ... + * ], + * ... + * ], + * + * @param string $xml_string The XML string to convert + * @return array The associative array structure + */ + public function convertMySQLDumpXMLDataToArrayStructure(string $xml_string): array + { + $dom = new \DOMDocument(); + $dom->loadXML($xml_string); + + //Check that the root node is a node + $root = $dom->documentElement; + if ($root->nodeName !== 'mysqldump') { + throw new \InvalidArgumentException('The given XML string is not a valid MySQL dump XML file!'); + } + + //Get all nodes (there must be exactly one) + $databases = $root->getElementsByTagName('database'); + if ($databases->length !== 1) { + throw new \InvalidArgumentException('The given XML string is not a valid MySQL dump XML file!'); + } + + //Get the node + $database = $databases->item(0); + + //Get all nodes + $tables = $database->getElementsByTagName('table_data'); + $table_data = []; + + //Iterate over all nodes and convert them to arrays + foreach ($tables as $table) { + $table_data[$table->getAttribute('name')] = $this->convertTableToArray($table); + } + + return $table_data; + } + + private function convertTableToArray(\DOMElement $table): array + { + $table_data = []; + + //Get all nodes + $rows = $table->getElementsByTagName('row'); + + //Iterate over all nodes and convert them to arrays + foreach ($rows as $row) { + $table_data[] = $this->convertTableRowToArray($row); + } + + return $table_data; + } + + private function convertTableRowToArray(\DOMElement $table_row): array + { + $row_data = []; + + //Get all nodes + $fields = $table_row->getElementsByTagName('field'); + + //Iterate over all nodes + foreach ($fields as $field) { + $row_data[$field->getAttribute('name')] = $field->nodeValue; + } + + return $row_data; + } +} \ No newline at end of file diff --git a/tests/Services/Misc/MySQLDumpXMLConverterTest.php b/tests/Services/Misc/MySQLDumpXMLConverterTest.php new file mode 100644 index 00000000..40ef2718 --- /dev/null +++ b/tests/Services/Misc/MySQLDumpXMLConverterTest.php @@ -0,0 +1,54 @@ +. + */ + +namespace App\Tests\Services\Misc; + +use App\Services\Misc\MySQLDumpXMLConverter; +use PHPUnit\Framework\TestCase; + +class MySQLDumpXMLConverterTest extends TestCase +{ + + public function testConvertMySQLDumpXMLDataToArrayStructure() + { + $service = new MySQLDumpXMLConverter(); + + //Load the test XML file + $xml_string = file_get_contents(__DIR__.'/../../assets/partkeepr_import_test.xml'); + + $result = $service->convertMySQLDumpXMLDataToArrayStructure($xml_string); + + //Check that the result is an array + $this->assertIsArray($result); + + //Must contain 36 tables + $this->assertCount(50, $result); + + //Must have a table called "footprints" + $this->assertArrayHasKey('footprint', $result); + + //Must have 36 entry in the "footprints" table + $this->assertCount(36, $result['footprint']); + + $this->assertSame('1', $result['footprint'][0]['id']); + $this->assertSame('CBGA-32', $result['footprint'][0]['name']); + + } +} diff --git a/tests/assets/partkeepr_import_test.xml b/tests/assets/partkeepr_import_test.xml new file mode 100644 index 00000000..4fa497e2 --- /dev/null +++ b/tests/assets/partkeepr_import_test.xml @@ -0,0 +1,8952 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3 + 2023-03-22 23:28:48 + partkeepr:cron:synctips + + + 4 + 2023-03-22 23:28:48 + partkeepr:cron:create-statistic-snapshot + + + + + + + + + + + + + + + + + + + + 1 + Reichelt + Bal + www.ggllg.de + flksl + dsf + fdlsf + dfs + Re + 0 + + + 2 + rewr + dsf + sf + sdf + sf + sf + sf + sdf + 0 + + + + + + + + + + + + + + + 1 + 3 + CBGA-32 + 32-Lead Ceramic Ball Grid Array + + + 2 + 5 + FCBGA-576 + 576-Ball Ball Grid Array, Thermally Enhanced + + + 3 + 7 + PBGA-119 + 119-Ball Plastic Ball Grid Array + + + 4 + 9 + PBGA-169 + 169-Ball Plastic Ball Grid Array + + + 5 + 11 + PBGA-225 + 225-Ball Plastic a Ball Grid Array + + + 6 + 13 + PBGA-260 + 260-Ball Plastic Ball Grid Array + + + 7 + 15 + PBGA-297 + 297-Ball Plastic Ball Grid Array + + + 8 + 17 + PBGA-304 + 304-Lead Plastic Ball Grid Array + + + 9 + 19 + PBGA-316 + 316-Lead Plastic Ball Grid Array + + + 10 + 21 + PBGA-324 + 324-Ball Plastic Ball Grid Array + + + 11 + 23 + PBGA-385 + 385-Lead Ball Grid Array + + + 12 + 25 + PBGA-400 + 400-Ball Plastic Ball Grid Array + + + 13 + 27 + PBGA-484 + 484-Ball Plastic Ball Grid Array + + + 14 + 29 + PBGA-625 + 625-Ball Plastic Ball Grid Array + + + 15 + 31 + PBGA-676 + 676-Ball Plastic Ball Grid Array + + + 16 + 33 + SBGA-256 + 256-Ball Ball Grid Array, Thermally Enhanced + + + 17 + 35 + SBGA-304 + 304-Ball Ball Grid Array, Thermally Enhanced + + + 18 + 37 + SBGA-432 + 432-Ball Ball Grid Array, Thermally Enhanced + + + 19 + 39 + CerDIP-8 + 8-Lead Ceramic Dual In-Line Package + + + 20 + 41 + CerDIP-14 + 14-Lead Ceramic Dual In-Line Package + + + 21 + 43 + CerDIP-16 + 16-Lead Ceramic Dual In-Line Package + + + 22 + 45 + CerDIP-18 + 18-Lead Ceramic Dual In-Line Package + + + 23 + 47 + CerDIP-20 + 20-Lead Ceramic Dual In-Line Package + + + 24 + 49 + CerDIP-24 Narrow + 24-Lead Ceramic Dual In-Line Package - Narrow Body + + + 25 + 51 + CerDIP-24 Wide + 24-Lead Ceramic Dual In-Line Package - Wide Body + + + 26 + 53 + CerDIP-28 + 28-Lead Ceramic Dual In-Line Package + + + 27 + 55 + CerDIP-40 + 40-Lead Ceramic Dual In-Line Package + + + 28 + 57 + PDIP-8 + 8-Lead Plastic Dual In-Line Package + + + 29 + 59 + PDIP-14 + 14-Lead Plastic Dual In-Line Package + + + 30 + 61 + PDIP-16 + 16-Lead Plastic Dual In-Line Package + + + 31 + 63 + PDIP-18 + 18-Lead Plastic Dual In-Line Package + + + 32 + 65 + PDIP-20 + 20-Lead Plastic Dual In-Line Package + + + 33 + 67 + PDIP-24 + 24-Lead Plastic Dual In-Line Package + + + 34 + 69 + PDIP-28 Narrow + 28-Lead Plastic Dual In-Line Package, Narrow Body + + + 35 + 71 + PDIP-28 Wide + 28-Lead Plastic Dual In-Line Package, Wide Body + + + 36 + + SOIC-N-EP-8 + 8-Lead Standard Small Outline Package, with Expose Pad + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + 142 + 0 + 1 + Root Category + + Root Category + + + 2 + 1 + 2 + 5 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 3 + 2 + 3 + 4 + 2 + 1 + CBGA + + Root Category ➤ BGA ➤ CBGA + + + 4 + 1 + 6 + 9 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 5 + 4 + 7 + 8 + 2 + 1 + FCBGA + + Root Category ➤ BGA ➤ FCBGA + + + 6 + 1 + 10 + 13 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 7 + 6 + 11 + 12 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 8 + 1 + 14 + 17 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 9 + 8 + 15 + 16 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 10 + 1 + 18 + 21 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 11 + 10 + 19 + 20 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 12 + 1 + 22 + 25 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 13 + 12 + 23 + 24 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 14 + 1 + 26 + 29 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 15 + 14 + 27 + 28 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 16 + 1 + 30 + 33 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 17 + 16 + 31 + 32 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 18 + 1 + 34 + 37 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 19 + 18 + 35 + 36 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 20 + 1 + 38 + 41 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 21 + 20 + 39 + 40 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 22 + 1 + 42 + 45 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 23 + 22 + 43 + 44 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 24 + 1 + 46 + 49 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 25 + 24 + 47 + 48 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 26 + 1 + 50 + 53 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 27 + 26 + 51 + 52 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 28 + 1 + 54 + 57 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 29 + 28 + 55 + 56 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 30 + 1 + 58 + 61 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 31 + 30 + 59 + 60 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 32 + 1 + 62 + 65 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 33 + 32 + 63 + 64 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 34 + 1 + 66 + 69 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 35 + 34 + 67 + 68 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 36 + 1 + 70 + 73 + 1 + 1 + BGA + + Root Category ➤ BGA + + + 37 + 36 + 71 + 72 + 2 + 1 + PBGA + + Root Category ➤ BGA ➤ PBGA + + + 38 + 1 + 74 + 77 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 39 + 38 + 75 + 76 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 40 + 1 + 78 + 81 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 41 + 40 + 79 + 80 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 42 + 1 + 82 + 85 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 43 + 42 + 83 + 84 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 44 + 1 + 86 + 89 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 45 + 44 + 87 + 88 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 46 + 1 + 90 + 93 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 47 + 46 + 91 + 92 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 48 + 1 + 94 + 97 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 49 + 48 + 95 + 96 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 50 + 1 + 98 + 101 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 51 + 50 + 99 + 100 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 52 + 1 + 102 + 105 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 53 + 52 + 103 + 104 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 54 + 1 + 106 + 109 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 55 + 54 + 107 + 108 + 2 + 1 + CERDIP + + Root Category ➤ DIP ➤ CERDIP + + + 56 + 1 + 110 + 113 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 57 + 56 + 111 + 112 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 58 + 1 + 114 + 117 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 59 + 58 + 115 + 116 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 60 + 1 + 118 + 121 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 61 + 60 + 119 + 120 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 62 + 1 + 122 + 125 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 63 + 62 + 123 + 124 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 64 + 1 + 126 + 129 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 65 + 64 + 127 + 128 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 66 + 1 + 130 + 133 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 67 + 66 + 131 + 132 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 68 + 1 + 134 + 137 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 69 + 68 + 135 + 136 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + 70 + 1 + 138 + 141 + 1 + 1 + DIP + + Root Category ➤ DIP + + + 71 + 70 + 139 + 140 + 2 + 1 + PDIP + + Root Category ➤ DIP ➤ PDIP + + + + + + + + + + + + + + + + + + + + 1 + 1 + footprint + 55f695c0-c8ff-11ed-bdcf-59f5f5013ecd + CBGA-32.png + image/png + 23365 + png + + 2023-03-22 23:17:30 + + + 2 + 2 + footprint + 55f7eb50-c8ff-11ed-8d5f-bf44eda6855c + FCBGA-576.png + image/png + 47861 + png + + 2023-03-22 23:17:30 + + + 3 + 3 + footprint + 55f8444c-c8ff-11ed-9ff3-21919bd0b36e + PBGA-119.png + image/png + 32537 + png + + 2023-03-22 23:17:30 + + + 4 + 4 + footprint + 55f8921c-c8ff-11ed-b733-1f9d0f81d4fc + PBGA-169.png + image/png + 36699 + png + + 2023-03-22 23:17:30 + + + 5 + 5 + footprint + 55f8e42e-c8ff-11ed-9d48-a25b23bb490e + PBGA-225.png + image/png + 39366 + png + + 2023-03-22 23:17:30 + + + 6 + 6 + footprint + 55f93226-c8ff-11ed-924b-2b812e93d1f6 + PBGA-260.png + image/png + 61202 + png + + 2023-03-22 23:17:30 + + + 7 + 7 + footprint + 55f97e48-c8ff-11ed-aa4b-1e57132db8dd + PBGA-297.png + image/png + 68013 + png + + 2023-03-22 23:17:30 + + + 8 + 8 + footprint + 55f9ce34-c8ff-11ed-938d-dc132c365553 + PBGA-304.png + image/png + 55833 + png + + 2023-03-22 23:17:30 + + + 9 + 9 + footprint + 55fa1808-c8ff-11ed-9c31-69685fea3a6f + PBGA-316.png + image/png + 55996 + png + + 2023-03-22 23:17:30 + + + 10 + 10 + footprint + 55fa66e6-c8ff-11ed-b7b8-21795f745a53 + PBGA-324.png + image/png + 44882 + png + + 2023-03-22 23:17:30 + + + 11 + 11 + footprint + 55fab330-c8ff-11ed-b5d1-2422489f775a + PBGA-385.png + image/png + 35146 + png + + 2023-03-22 23:17:30 + + + 12 + 12 + footprint + 55fafcaa-c8ff-11ed-ae25-dd7dc4b216e3 + PBGA-400.png + image/png + 67933 + png + + 2023-03-22 23:17:30 + + + 13 + 13 + footprint + 55fb4a16-c8ff-11ed-9bf7-ba491078c6c7 + PBGA-484.png + image/png + 49851 + png + + 2023-03-22 23:17:30 + + + 14 + 14 + footprint + 55fb93ae-c8ff-11ed-bb52-0c76c2603549 + PBGA-625.png + image/png + 65307 + png + + 2023-03-22 23:17:30 + + + 15 + 15 + footprint + 55fbdeae-c8ff-11ed-8e4d-16e3121e623a + PBGA-676.png + image/png + 54708 + png + + 2023-03-22 23:17:30 + + + 16 + 16 + footprint + 55fc2814-c8ff-11ed-ab90-26137e083580 + SBGA-256.png + image/png + 48636 + png + + 2023-03-22 23:17:30 + + + 17 + 17 + footprint + 55fc6fea-c8ff-11ed-8671-25ee225cc8a6 + SBGA-304.png + image/png + 51944 + png + + 2023-03-22 23:17:30 + + + 18 + 18 + footprint + 55fcb91e-c8ff-11ed-ab2c-dcfa807f34fd + SBGA-432.png + image/png + 63247 + png + + 2023-03-22 23:17:30 + + + 19 + 19 + footprint + 55fd0720-c8ff-11ed-bf3f-b34a2375c258 + CERDIP-8.png + image/png + 13544 + png + + 2023-03-22 23:17:30 + + + 20 + 20 + footprint + 55fd4974-c8ff-11ed-8c41-52e85be33ce3 + CERDIP-14.png + image/png + 14226 + png + + 2023-03-22 23:17:30 + + + 21 + 21 + footprint + 55fd92e4-c8ff-11ed-b58b-2e0b4703c07c + CERDIP-16.png + image/png + 14576 + png + + 2023-03-22 23:17:30 + + + 22 + 22 + footprint + 55fdd718-c8ff-11ed-aa74-6ebd8a44b22c + CERDIP-18.png + image/png + 9831 + png + + 2023-03-22 23:17:30 + + + 23 + 23 + footprint + 55fe28f8-c8ff-11ed-9be0-45de90527ab0 + CERDIP-20.png + image/png + 10209 + png + + 2023-03-22 23:17:30 + + + 24 + 24 + footprint + 55fe7088-c8ff-11ed-9cbd-225ebb21aad1 + CERDIP-24-N.png + image/png + 11582 + png + + 2023-03-22 23:17:30 + + + 25 + 25 + footprint + 55feb516-c8ff-11ed-82b1-60555b199ba1 + CERDIP-24-W.png + image/png + 12407 + png + + 2023-03-22 23:17:30 + + + 26 + 26 + footprint + 55fefb3e-c8ff-11ed-bfbd-8ff7ca6d8389 + CERDIP-28.png + image/png + 12233 + png + + 2023-03-22 23:17:30 + + + 27 + 27 + footprint + 55ff4346-c8ff-11ed-8e5d-3cb198a6abbe + CERDIP-40.png + image/png + 12421 + png + + 2023-03-22 23:17:30 + + + 28 + 28 + footprint + 55ff87d4-c8ff-11ed-b606-58804e50e34e + PDIP-8.png + image/png + 13537 + png + + 2023-03-22 23:17:30 + + + 29 + 29 + footprint + 55ffcdca-c8ff-11ed-9a42-f03c778f9d7b + PDIP-14.png + image/png + 13779 + png + + 2023-03-22 23:17:30 + + + 30 + 30 + footprint + 56001370-c8ff-11ed-8386-42396433ea07 + PDIP-16.png + image/png + 18305 + png + + 2023-03-22 23:17:30 + + + 31 + 31 + footprint + 56005c5e-c8ff-11ed-b2ce-1252ddf0e3d3 + PDIP-18.png + image/png + 14893 + png + + 2023-03-22 23:17:30 + + + 32 + 32 + footprint + 5600abbe-c8ff-11ed-8cdf-327a4488b1d8 + PDIP-20.png + image/png + 14429 + png + + 2023-03-22 23:17:30 + + + 33 + 33 + footprint + 5600eeb2-c8ff-11ed-b438-41b6b6a9c181 + PDIP-24.png + image/png + 14647 + png + + 2023-03-22 23:17:30 + + + 34 + 34 + footprint + 560130a2-c8ff-11ed-9c9e-b5df2af3c7c7 + PDIP-28-N.png + image/png + 18703 + png + + 2023-03-22 23:17:30 + + + 35 + 35 + footprint + 560176e8-c8ff-11ed-bbe8-26c989058206 + PDIP-28-W.png + image/png + 15728 + png + + 2023-03-22 23:17:30 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + partkeepr + partkeepr + 1 + flbvx2z49kgs04gw0cs08gc4g88kcgk + 9Nxsm8JzMh+zZ+seR5wv/r42NGtzJmeL1IC9LD1IaX19z+YRewwUs0UrIx7sjOhjRn8bkAusm/IKlRGNizkKhw== + + 0 + 0 + + + + a:1:{i:0;s:16:"ROLE_SUPER_ADMIN";} + 0 + + partkeepr@test.org + partkeepr@test.org + + + 2 + partkeepr2 + partkeepr2 + 1 + ocy7yd6dxlw4wo84c0sosw0kg8k8gcs + a+h/FoVvBEoEGq9wAwbsXyD8RywH1qzciav8FR6ekv+UP6UHl+h+TCN2JTlIK8emTkRhyE3sXPtbH8TAQ0XOYw== + + 0 + 0 + + + + a:0:{} + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + Integrated Circuit Designs + + + + + + + + + 2 + ACTEL + + + + + + + + + 3 + ALTINC + + + + + + + + + 4 + Aeroflex + + + + + + + + + 5 + Agilent Technologies + + + + + + + + + 6 + AKM Semiconductor + + + + + + + + + 7 + Alesis Semiconductor + + + + + + + + + 8 + ALi (Acer Laboratories Inc.) + + + + + + + + + 9 + Allayer Communications + + + + + + + + + 10 + Allegro Microsystems + + + + + + + + + 11 + Alliance Semiconductor + + + + + + + + + 12 + Alpha Industries + + + + + + + + + 13 + Alpha Microelectronics + + + + + + + + + 14 + Altera + + + + + + + + + 15 + Advanced Micro Devices (AMD) + + + + + + + + + 16 + American Microsystems, Inc. (AMI) + + + + + + + + + 17 + Amic Technology + + + + + + + + + 18 + Amphus + + + + + + + + + 19 + Anachip Corp. + + + + + + + + + 20 + ANADIGICs + + + + + + + + + 21 + Analog Devices + + + + + + + + + 22 + Analog Systems + + + + + + + + + 23 + Anchor Chips + + + + + + + + + 24 + Apex Microtechnology + + + + + + + + + 25 + ARK Logic + + + + + + + + + 26 + ASD + + + + + + + + + 27 + Astec Semiconductor + + + + + + + + + 28 + ATC (Analog Technologie) + + + + + + + + + 29 + ATecoM + + + + + + + + + 30 + ATI Technologies + + + + + + + + + 31 + Atmel + + + + + + + + + 32 + AT&T + + + + + + + + + 33 + AudioCodes + + + + + + + + + 34 + Aura Vision + + + + + + + + + 35 + Aureal + + + + + + + + + 36 + Austin Semiconductor + + + + + + + + + 37 + Avance Logic + + + + + + + + + 38 + Bel Fuse + + + + + + + + + 39 + Benchmarq Microelectronics + + + + + + + + + 40 + BI Technologies + + + + + + + + + 41 + Bowmar/White + + + + + + + + + 42 + Brightflash + + + + + + + + + 43 + Broadcom + + + + + + + + + 44 + Brooktree(now Rockwell) + + + + + + + + + 45 + Burr Brown + + + + + + + + + 46 + California Micro Devices + + + + + + + + + 47 + Calogic + + + + + + + + + 48 + Catalyst Semiconductor + + + + + + + + + 49 + Centon Electronics + + + + + + + + + 50 + Ceramate Technical + + + + + + + + + 51 + Cherry Semiconductor + + + + + + + + + 52 + Chipcon AS + + + + + + + + + 53 + Chips + + + + + + + + + 54 + Chrontel + + + + + + + + + 55 + Cirrus Logic + + + + + + + + + 56 + ComCore Semiconductor + + + + + + + + + 57 + Conexant + + + + + + + + + 58 + Cosmo Electronics + + + + + + + + + 59 + Chrystal + + + + + + + + + 60 + Cygnal + + + + + + + + + 61 + Cypress Semiconductor + + + + + + + + + 62 + Cyrix Corporation + + + + + + + + + 63 + Daewoo Electronics Semiconductor + + + + + + + + + 64 + Dallas Semiconductor + + + + + + + + + 65 + Davicom Semiconductor + + + + + + + + + 66 + Data Delay Devices + + + + + + + + + 67 + Diamond Technologies + + + + + + + + + 68 + DIOTEC + + + + + + + + + 69 + DTC Data Technology + + + + + + + + + 70 + DVDO + + + + + + + + + 71 + EG&G + + + + + + + + + 72 + Elan Microelectronics + + + + + + + + + 73 + ELANTEC + + + + + + + + + 74 + Electronic Arrays + + + + + + + + + 75 + Elite Flash Storage Technology Inc. (EFST) + + + + + + + + + 76 + EM Microelectronik - Marin + + + + + + + + + 77 + Enhanced Memory Systems + + + + + + + + + 78 + Ensoniq Corp + + + + + + + + + 79 + EON Silicon Devices + + + + + + + + + 80 + Epson + + + + + + + + + 81 + Ericsson + + + + + + + + + 82 + ESS Technology + + + + + + + + + 83 + Electronic Technology + + + + + + + + + 84 + EXAR + + + + + + + + + 85 + Excel Semiconductor Inc. + + + + + + + + + 86 + Fairschild + + + + + + + + + 87 + Freescale Semiconductor + + + + + + + + + 88 + Fujitsu + + + + + + + + + 89 + Galileo Technology + + + + + + + + + 90 + Galvantech + + + + + + + + + 91 + GEC Plessey + + + + + + + + + 92 + Gennum + + + + + + + + + 93 + General Electric (Harris) + + + + + + + + + 94 + General Instruments + + + + + + + + + 95 + G-Link Technology + + + + + + + + + 96 + Goal Semiconductor + + + + + + + + + 97 + Goldstar + + + + + + + + + 98 + Gould + + + + + + + + + 99 + Greenwich Instruments + + + + + + + + + 100 + General Semiconductor + + + + + + + + + 101 + Harris Semiconductor + + + + + + + + + 102 + VEB + + + + + + + + + 103 + Hitachi Semiconductor + + + + + + + + + 104 + Holtek + + + + + + + + + 105 + Hewlett Packard + + + + + + + + + 106 + Hualon + + + + + + + + + 107 + Hynix Semiconductor + + + + + + + + + 108 + Hyundai + + + + + + + + + 109 + IC Design + + + + + + + + + 110 + Integrated Circuit Systems (ICS) + + + + + + + + + 111 + IC - Haus + + + + + + + + + 112 + ICSI (Integrated Circuit Solution Inc.) + + + + + + + + + 113 + I-Cube + + + + + + + + + 114 + IC Works + + + + + + + + + 115 + Integrated Device Technology (IDT) + + + + + + + + + 116 + IGS Technologies + + + + + + + + + 117 + IMPALA Linear + + + + + + + + + 118 + IMP + + + + + + + + + 119 + Infineon + + + + + + + + + 120 + INMOS + + + + + + + + + 121 + Intel + + + + + + + + + 122 + Intersil + + + + + + + + + 123 + International Rectifier + + + + + + + + + 124 + Information Storage Devices + + + + + + + + + 125 + ISSI (Integrated Silicon Solution, Inc.) + + + + + + + + + 126 + Integrated Technology Express + + + + + + + + + 127 + ITT Semiconductor (Micronas Intermetall) + + + + + + + + + 128 + IXYS + + + + + + + + + 129 + Korea Electronics (KEC) + + + + + + + + + 130 + Kota Microcircuits + + + + + + + + + 131 + Lattice Semiconductor Corp. + + + + + + + + + 132 + Lansdale Semiconductor + + + + + + + + + 133 + Level One Communications + + + + + + + + + 134 + LG Semicon (Lucky Goldstar Electronic Co.) + + + + + + + + + 135 + Linear Technology + + + + + + + + + 136 + Linfinity Microelectronics + + + + + + + + + 137 + Lite-On + + + + + + + + + 138 + Lucent Technologies (AT&T Microelectronics) + + + + + + + + + 139 + Macronix International + + + + + + + + + 140 + Marvell Semiconductor + + + + + + + + + 141 + Matsushita Panasonic + + + + + + + + + 142 + Maxim Dallas + + + + + + + + + 143 + Media Vision + + + + + + + + + 144 + Microchip (Arizona Michrochip Technology) + + + + + + + + + 145 + Matra MHS + + + + + + + + + 146 + Micrel Semiconductor + + + + + + + + + 147 + Micronas + + + + + + + + + 148 + Micronix Integrated Systems + + + + + + + + + 149 + Micron Technology, Inc. + + + + + + + + + 150 + Microsemi + + + + + + + + + 151 + Mini-Circuits + + + + + + + + + 152 + Mitel Semiconductor + + + + + + + + + 153 + Mitsubishi Semiconductor + + + + + + + + + 154 + Micro Linear + + + + + + + + + 155 + MMI (Monolithic Memories, Inc.) + + + + + + + + + 156 + Mosaic Semiconductor + + + + + + + + + 157 + Mosel Vitelic + + + + + + + + + 158 + MOS Technologies + + + + + + + + + 159 + Mostek + + + + + + + + + 160 + MoSys + + + + + + + + + 161 + Motorola + + + + + + + + + 162 + Microtune + + + + + + + + + 163 + M-Systems + + + + + + + + + 164 + Murata Manufacturing + + + + + + + + + 165 + MWave (IBM) + + + + + + + + + 166 + Myson Technology + + + + + + + + + 167 + NEC Electronics + + + + + + + + + 168 + NexFlash Technologies + + + + + + + + + 169 + New Japan Radio + + + + + + + + + 170 + National Semiconductor + + + + + + + + + 171 + NVidia Corporation + + + + + + + + + 172 + Oak Technology + + + + + + + + + 173 + Oki Semiconductor + + + + + + + + + 174 + Opti + + + + + + + + + 175 + Orbit Semiconductor + + + + + + + + + 176 + Oren Semiconductor + + + + + + + + + 177 + Performance Semiconductor + + + + + + + + + 178 + Pericom Semiconductor + + + + + + + + + 179 + PhaseLink Laboratories + + + + + + + + + 180 + Philips Semiconductor + + + + + + + + + 181 + PLX Technology + + + + + + + + + 182 + PMC- Sierra + + + + + + + + + 183 + Precision Monolithics + + + + + + + + + 184 + Princeton Technology + + + + + + + + + 185 + PowerSmart + + + + + + + + + 186 + QuickLogic + + + + + + + + + 187 + Qlogic + + + + + + + + + 188 + Quality Semiconductor + + + + + + + + + 189 + Rabbit Semiconductor + + + + + + + + + 190 + Ramtron International Co. + + + + + + + + + 191 + Raytheon Semiconductor + + + + + + + + + 192 + RCA Solid State + + + + + + + + + 193 + Realtek Semiconductor + + + + + + + + + 194 + Rectron + + + + + + + + + 195 + Rendition + + + + + + + + + 196 + Renesas Technology + + + + + + + + + 197 + Rockwell + + + + + + + + + 198 + Rohm Corp. + + + + + + + + + 199 + S3 + + + + + + + + + 200 + Sage + + + + + + + + + 201 + Saifun Semiconductors Ltd. + + + + + + + + + 202 + Sames + + + + + + + + + 203 + Samsung + + + + + + + + + 204 + Sanken + + + + + + + + + 205 + Sanyo + + + + + + + + + 206 + Scenix + + + + + + + + + 207 + Samsung Electronics + + + + + + + + + 208 + SEEQ Technology + + + + + + + + + 209 + Seiko Instruments + + + + + + + + + 210 + Semtech + + + + + + + + + 211 + SGS-Ates + + + + + + + + + 212 + SGS-Thomson Microelectonics ST-M) + + + + + + + + + 213 + Sharp Microelectronics (USA) + + + + + + + + + 214 + Shindengen + + + + + + + + + 215 + Siemens Microelectronics, Inc. + + + + + + + + + 216 + Sierra + + + + + + + + + 217 + Sigma Tel + + + + + + + + + 218 + Signetics + + + + + + + + + 219 + Silicon Laboratories + + + + + + + + + 220 + Silicon Magic + + + + + + + + + 221 + Simtec Corp. + + + + + + + + + 222 + Siliconix + + + + + + + + + 223 + Siliconians + + + + + + + + + 224 + Sipex + + + + + + + + + 225 + Silicon Integrated Systems + + + + + + + + + 226 + SMC + + + + + + + + + 227 + Standard Microsystems + + + + + + + + + 228 + Sony Semiconductor + + + + + + + + + 229 + Space Electronics + + + + + + + + + 230 + Spectek + + + + + + + + + 231 + Signal Processing Technologies + + + + + + + + + 232 + Solid State Scientific + + + + + + + + + 233 + Silicon Storage Technology (SST) + + + + + + + + + 234 + STMicroelectronics + + + + + + + + + 235 + SUMMIT Microelectronics + + + + + + + + + 236 + Synergy Semiconductor + + + + + + + + + 237 + Synertek + + + + + + + + + 238 + Taiwan Semiconductor + + + + + + + + + 239 + TDK Semiconductor + + + + + + + + + 240 + Teccor Electronics + + + + + + + + + 241 + TelCom Semiconductor + + + + + + + + + 242 + Teledyne + + + + + + + + + 243 + Telefunken + + + + + + + + + 244 + Teltone + + + + + + + + + 245 + Thomson-CSF + + + + + + + + + 246 + Texas Instruments + + + + + + + + + 247 + Toko Amerika + + + + + + + + + 248 + Toshiba (US) + + + + + + + + + 249 + Trident + + + + + + + + + 250 + TriQuint Semiconductor + + + + + + + + + 251 + Triscend + + + + + + + + + 252 + Tseng Labs + + + + + + + + + 253 + Tundra + + + + + + + + + 254 + Turbo IC + + + + + + + + + 255 + Ubicom + + + + + + + + + 256 + United Microelectronics Corp (UMC) + + + + + + + + + 257 + Unitrode + + + + + + + + + 258 + USAR Systems + + + + + + + + + 259 + United Technologies Microelectronics Center (UTMC) + + + + + + + + + 260 + Utron + + + + + + + + + 261 + V3 Semiconductor + + + + + + + + + 262 + Vadem + + + + + + + + + 263 + Vanguard International Semiconductor + + + + + + + + + 264 + Vantis + + + + + + + + + 265 + Via Technologies + + + + + + + + + 266 + Virata + + + + + + + + + 267 + Vishay + + + + + + + + + 268 + Vision Tech + + + + + + + + + 269 + Vitelic + + + + + + + + + 270 + VLSI Technology + + + + + + + + + 271 + Volterra + + + + + + + + + 272 + VTC + + + + + + + + + 273 + Waferscale Integration (WSI) + + + + + + + + + 274 + Western Digital + + + + + + + + + 275 + Weitek + + + + + + + + + 276 + Winbond + + + + + + + + + 277 + Wofson Microelectronics + + + + + + + + + 278 + Xwmics + + + + + + + + + 279 + Xicor + + + + + + + + + 280 + Xilinx + + + + + + + + + 281 + Yamaha + + + + + + + + + 282 + Zetex Semiconductors + + + + + + + + + 283 + Zilog + + + + + + + + + 284 + ZMD (Zentrum Mikroelektronik Dresden) + + + + + + + + + 285 + Zoran + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + iclogo + 56736e10-c8ff-11ed-9738-a49472685cd5 + acer.png + image/png + 2195 + png + + 2023-03-22 23:17:31 + + + 2 + 2 + iclogo + 5673df12-c8ff-11ed-95b5-5825d19874bd + actel.png + image/png + 5003 + png + + 2023-03-22 23:17:31 + + + 3 + 3 + iclogo + 56740a14-c8ff-11ed-8a7a-e6d789965eb7 + advldev.png + image/png + 1835 + png + + 2023-03-22 23:17:31 + + + 4 + 4 + iclogo + 56743106-c8ff-11ed-9369-adae9b0aee92 + aeroflex1.png + image/png + 9649 + png + + 2023-03-22 23:17:31 + + + 5 + 4 + iclogo + 56744056-c8ff-11ed-9b85-834fc0efcc0c + aeroflex2.png + image/png + 4562 + png + + 2023-03-22 23:17:31 + + + 6 + 5 + iclogo + 56746626-c8ff-11ed-90f1-e9f5ae8d1b89 + agilent.png + image/png + 5264 + png + + 2023-03-22 23:17:31 + + + 7 + 6 + iclogo + 56748fde-c8ff-11ed-9322-3aae2ee0bd68 + akm.png + image/png + 2204 + png + + 2023-03-22 23:17:31 + + + 8 + 7 + iclogo + 5674b694-c8ff-11ed-b38e-10b020c2f3f7 + alesis.png + image/png + 1475 + png + + 2023-03-22 23:17:31 + + + 9 + 8 + iclogo + 5674de4e-c8ff-11ed-9606-a90fbac9c900 + ali1.png + image/png + 2462 + png + + 2023-03-22 23:17:31 + + + 10 + 8 + iclogo + 5674ea88-c8ff-11ed-8f21-b848e9e67a0f + ali2.png + image/png + 1784 + png + + 2023-03-22 23:17:31 + + + 11 + 9 + iclogo + 56751468-c8ff-11ed-bc15-3950afd683d4 + allayer.png + image/png + 1869 + png + + 2023-03-22 23:17:31 + + + 12 + 10 + iclogo + 56753d3a-c8ff-11ed-8973-2a1853f53aa3 + allegro.png + image/png + 1475 + png + + 2023-03-22 23:17:31 + + + 13 + 11 + iclogo + 567564e0-c8ff-11ed-8a16-05ef910cc7d3 + alliance.png + image/png + 1949 + png + + 2023-03-22 23:17:31 + + + 14 + 12 + iclogo + 56758ae2-c8ff-11ed-9328-593d60e003b6 + alphaind.png + image/png + 1403 + png + + 2023-03-22 23:17:31 + + + 15 + 13 + iclogo + 5675b22e-c8ff-11ed-a6f1-f0044c7db7ef + alphamic.png + image/png + 2989 + png + + 2023-03-22 23:17:31 + + + 16 + 13 + iclogo + 5675bdc8-c8ff-11ed-a4d8-e936fcf8a6f4 + alpha.png + image/png + 1534 + png + + 2023-03-22 23:17:31 + + + 17 + 14 + iclogo + 5675e406-c8ff-11ed-9f4d-85fa9b728109 + altera.png + image/png + 4064 + png + + 2023-03-22 23:17:31 + + + 18 + 15 + iclogo + 56760b2a-c8ff-11ed-9d5d-b60428ce67da + amd.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 19 + 16 + iclogo + 567631f4-c8ff-11ed-beda-6fc6e0010be3 + ami1.png + image/png + 2399 + png + + 2023-03-22 23:17:31 + + + 20 + 16 + iclogo + 56763d02-c8ff-11ed-916d-a5dc5999c8e1 + ami2.png + image/png + 1706 + png + + 2023-03-22 23:17:31 + + + 21 + 17 + iclogo + 56778702-c8ff-11ed-827a-4888d34a5221 + amic.png + image/png + 2228 + png + + 2023-03-22 23:17:31 + + + 22 + 18 + iclogo + 5677b72c-c8ff-11ed-9ea1-e94b2864ef6c + ampus.png + image/png + 6150 + png + + 2023-03-22 23:17:31 + + + 23 + 19 + iclogo + 5677e4cc-c8ff-11ed-a8c7-4149b345f008 + anachip.png + image/png + 3549 + png + + 2023-03-22 23:17:31 + + + 24 + 20 + iclogo + 56780bf0-c8ff-11ed-ad69-bead7e1692f4 + anadigic.png + image/png + 5147 + png + + 2023-03-22 23:17:31 + + + 25 + 21 + iclogo + 56783242-c8ff-11ed-a430-b6c923d43e60 + analog1.png + image/png + 1262 + png + + 2023-03-22 23:17:31 + + + 26 + 21 + iclogo + 56783c60-c8ff-11ed-86fa-f7503d83026f + analog.png + image/png + 1403 + png + + 2023-03-22 23:17:31 + + + 27 + 22 + iclogo + 56786b9a-c8ff-11ed-a379-889ffabbe28f + anasys.png + image/png + 3309 + png + + 2023-03-22 23:17:31 + + + 28 + 23 + iclogo + 56789250-c8ff-11ed-b016-86e9cd015aa9 + anchorch.png + image/png + 1475 + png + + 2023-03-22 23:17:31 + + + 29 + 24 + iclogo + 5678b9d8-c8ff-11ed-8f71-89d4834df732 + apex1.png + image/png + 2627 + png + + 2023-03-22 23:17:31 + + + 30 + 24 + iclogo + 5678c8ba-c8ff-11ed-8deb-a9146eb405f2 + apex.png + image/png + 3974 + png + + 2023-03-22 23:17:31 + + + 31 + 25 + iclogo + 5678eeee-c8ff-11ed-815b-c08fc768f909 + ark.png + image/png + 2089 + png + + 2023-03-22 23:17:31 + + + 32 + 26 + iclogo + 56791c7a-c8ff-11ed-bdec-09b2ccd10f57 + asd.png + image/png + 5024 + png + + 2023-03-22 23:17:31 + + + 33 + 27 + iclogo + 567945b0-c8ff-11ed-a0d1-638200f30d7e + astec.png + image/png + 3369 + png + + 2023-03-22 23:17:31 + + + 34 + 28 + iclogo + 56796dba-c8ff-11ed-a83f-16b5def8c7d2 + atc.png + image/png + 8660 + png + + 2023-03-22 23:17:31 + + + 35 + 29 + iclogo + 56799a1a-c8ff-11ed-b586-b9bcffe8ade4 + atecom.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 36 + 30 + iclogo + 5679bdba-c8ff-11ed-ace8-9745eb1c428a + ati.png + image/png + 2630 + png + + 2023-03-22 23:17:31 + + + 37 + 31 + iclogo + 5679e20e-c8ff-11ed-a786-dd09c1a44a45 + atmel.png + image/png + 2843 + png + + 2023-03-22 23:17:31 + + + 38 + 32 + iclogo + 567a0c48-c8ff-11ed-9074-34f1bcfe6e71 + att.png + image/png + 2816 + png + + 2023-03-22 23:17:31 + + + 39 + 33 + iclogo + 567a2fd4-c8ff-11ed-a43a-959314ab1731 + audiocod.png + image/png + 2429 + png + + 2023-03-22 23:17:31 + + + 40 + 34 + iclogo + 567a5414-c8ff-11ed-874a-b1fa27b6759f + auravis.png + image/png + 2281 + png + + 2023-03-22 23:17:31 + + + 41 + 35 + iclogo + 567a782c-c8ff-11ed-b1e0-163c79aca6f7 + aureal.png + image/png + 2109 + png + + 2023-03-22 23:17:31 + + + 42 + 36 + iclogo + 567a9eba-c8ff-11ed-ac82-563388522721 + austin.png + image/png + 2464 + png + + 2023-03-22 23:17:31 + + + 43 + 37 + iclogo + 567ac1c4-c8ff-11ed-b710-3f02bf90a788 + averlog.png + image/png + 1552 + png + + 2023-03-22 23:17:31 + + + 44 + 38 + iclogo + 567ae5f0-c8ff-11ed-8686-515d834485c9 + belfuse.png + image/png + 2204 + png + + 2023-03-22 23:17:31 + + + 45 + 39 + iclogo + 567b0ac6-c8ff-11ed-ba65-ddfaf11024ac + benchmrq.png + image/png + 1370 + png + + 2023-03-22 23:17:31 + + + 46 + 40 + iclogo + 567b2f60-c8ff-11ed-bb76-4a08e20c9f53 + bi.png + image/png + 2008 + png + + 2023-03-22 23:17:31 + + + 47 + 41 + iclogo + 567b529c-c8ff-11ed-bf78-8f32f9163954 + bowmar_white.png + image/png + 4652 + png + + 2023-03-22 23:17:31 + + + 48 + 42 + iclogo + 567b786c-c8ff-11ed-a2dc-555306aca428 + bright.png + image/png + 6839 + png + + 2023-03-22 23:17:31 + + + 49 + 43 + iclogo + 567b9db0-c8ff-11ed-8bfe-99239254da53 + broadcom.png + image/png + 6056 + png + + 2023-03-22 23:17:31 + + + 50 + 44 + iclogo + 567bc344-c8ff-11ed-994c-b5cf21979ad4 + brooktre.png + image/png + 1364 + png + + 2023-03-22 23:17:31 + + + 51 + 45 + iclogo + 567be96e-c8ff-11ed-9f51-6cab3d998613 + burrbrwn.png + image/png + 3563 + png + + 2023-03-22 23:17:31 + + + 52 + 46 + iclogo + 567c0d18-c8ff-11ed-b35c-4cab50b0cd73 + calmicro.png + image/png + 2109 + png + + 2023-03-22 23:17:31 + + + 53 + 47 + iclogo + 567c7532-c8ff-11ed-a3ea-b93c3e9c4635 + calogic.png + image/png + 3367 + png + + 2023-03-22 23:17:31 + + + 54 + 48 + iclogo + 567c9e0e-c8ff-11ed-8847-a0b673d5e1ab + catalys1.png + image/png + 1922 + png + + 2023-03-22 23:17:31 + + + 55 + 48 + iclogo + 567ca9bc-c8ff-11ed-8bbb-027650d63cd5 + catalyst.png + image/png + 2228 + png + + 2023-03-22 23:17:31 + + + 56 + 49 + iclogo + 567ccf14-c8ff-11ed-8b32-854f12739943 + ccube.png + image/png + 1309 + png + + 2023-03-22 23:17:31 + + + 57 + 50 + iclogo + 567cf700-c8ff-11ed-877f-fd2bffe83a5e + ceramate1.png + image/png + 2917 + png + + 2023-03-22 23:17:31 + + + 58 + 50 + iclogo + 567d00ba-c8ff-11ed-b7f4-5f940585884e + ceramate2.png + image/png + 2917 + png + + 2023-03-22 23:17:31 + + + 59 + 51 + iclogo + 567d21b2-c8ff-11ed-bf52-26280437fbc7 + cherry.png + image/png + 2507 + png + + 2023-03-22 23:17:31 + + + 60 + 52 + iclogo + 567d46e2-c8ff-11ed-906c-72aa9a6fbc85 + chipcon1.png + image/png + 8655 + png + + 2023-03-22 23:17:31 + + + 61 + 52 + iclogo + 567d533a-c8ff-11ed-bd42-4e0ad6fe5289 + chipcon2.png + image/png + 2923 + png + + 2023-03-22 23:17:31 + + + 62 + 53 + iclogo + 567d7a86-c8ff-11ed-9ebc-093fefd63927 + chips.png + image/png + 2864 + png + + 2023-03-22 23:17:31 + + + 63 + 54 + iclogo + 567d9f5c-c8ff-11ed-abd4-04b03743df6b + chrontel.png + image/png + 1476 + png + + 2023-03-22 23:17:31 + + + 64 + 55 + iclogo + 567dc4b4-c8ff-11ed-ac12-2ff653599de6 + cirrus.png + image/png + 3218 + png + + 2023-03-22 23:17:31 + + + 65 + 56 + iclogo + 567de854-c8ff-11ed-97a9-60bd3a522823 + comcore.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 66 + 57 + iclogo + 567e0bc2-c8ff-11ed-9c75-c05721ac16a9 + conexant.png + image/png + 2051 + png + + 2023-03-22 23:17:31 + + + 67 + 58 + iclogo + 567e3214-c8ff-11ed-aaef-57a02609532f + cosmo.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 68 + 59 + iclogo + 567e5618-c8ff-11ed-8a30-b4f5ca6ac96c + crystal.png + image/png + 3605 + png + + 2023-03-22 23:17:31 + + + 69 + 60 + iclogo + 567e7c2e-c8ff-11ed-b3d3-315f48719dc9 + cygnal.png + image/png + 2135 + png + + 2023-03-22 23:17:31 + + + 70 + 61 + iclogo + 567ee5ce-c8ff-11ed-87bd-3a00ca7c85ff + cypres1.png + image/png + 2504 + png + + 2023-03-22 23:17:31 + + + 71 + 61 + iclogo + 567ef9e2-c8ff-11ed-8ffd-6085c95108d8 + cypress.png + image/png + 4275 + png + + 2023-03-22 23:17:31 + + + 72 + 62 + iclogo + 567f29da-c8ff-11ed-91ba-029df33a5d24 + cyrix.png + image/png + 2204 + png + + 2023-03-22 23:17:31 + + + 73 + 63 + iclogo + 567f4f46-c8ff-11ed-9f77-98250e474335 + daewoo.png + image/png + 1907 + png + + 2023-03-22 23:17:31 + + + 74 + 64 + iclogo + 567f73d6-c8ff-11ed-9c34-bc9f54818bc6 + dallas1.png + image/png + 1469 + png + + 2023-03-22 23:17:31 + + + 75 + 64 + iclogo + 567f801a-c8ff-11ed-8430-b8998c93e36f + dallas2.png + image/png + 1309 + png + + 2023-03-22 23:17:31 + + + 76 + 64 + iclogo + 567f8a92-c8ff-11ed-a8fb-004a0856ee44 + dallas3.png + image/png + 1869 + png + + 2023-03-22 23:17:31 + + + 77 + 65 + iclogo + 567faef0-c8ff-11ed-b5bd-538ac90d1fd5 + davicom.png + image/png + 4589 + png + + 2023-03-22 23:17:31 + + + 78 + 66 + iclogo + 567fd3da-c8ff-11ed-b336-02ccc0a4e663 + ddd.png + image/png + 3235 + png + + 2023-03-22 23:17:31 + + + 79 + 67 + iclogo + 567ffa5e-c8ff-11ed-8db8-ac892f4a9795 + diamond.png + image/png + 2504 + png + + 2023-03-22 23:17:31 + + + 80 + 68 + iclogo + 568021f0-c8ff-11ed-a5b8-b53005039505 + diotec.png + image/png + 1454 + png + + 2023-03-22 23:17:31 + + + 81 + 69 + iclogo + 5680464e-c8ff-11ed-a2c3-a6b9353d79d0 + dtc1.png + image/png + 2513 + png + + 2023-03-22 23:17:31 + + + 82 + 69 + iclogo + 568050f8-c8ff-11ed-b7ad-db2eaa536a4e + dtc2.png + image/png + 1670 + png + + 2023-03-22 23:17:31 + + + 83 + 70 + iclogo + 5680bf70-c8ff-11ed-a042-1831bb8be007 + dvdo.png + image/png + 2357 + png + + 2023-03-22 23:17:31 + + + 84 + 71 + iclogo + 5680e66c-c8ff-11ed-af0e-c9899039e293 + egg.png + image/png + 1628 + png + + 2023-03-22 23:17:31 + + + 85 + 72 + iclogo + 56810cfa-c8ff-11ed-8fd5-bd7b625183a9 + elan.png + image/png + 13826 + png + + 2023-03-22 23:17:31 + + + 86 + 73 + iclogo + 5681384c-c8ff-11ed-ac8f-13c6d94c8c74 + elantec1.png + image/png + 1400 + png + + 2023-03-22 23:17:31 + + + 87 + 73 + iclogo + 56814350-c8ff-11ed-a12e-7b278c9ddb6e + elantec.png + image/png + 3274 + png + + 2023-03-22 23:17:31 + + + 88 + 74 + iclogo + 568165ce-c8ff-11ed-8c79-efdb39322df7 + elec_arrays.png + image/png + 5602 + png + + 2023-03-22 23:17:31 + + + 89 + 75 + iclogo + 56818b26-c8ff-11ed-9ee1-7ccf365558d1 + elite[1].png + image/png + 8285 + png + + 2023-03-22 23:17:31 + + + 90 + 76 + iclogo + 5681af16-c8ff-11ed-b0c2-eae2efd1511a + emmicro.png + image/png + 3599 + png + + 2023-03-22 23:17:31 + + + 91 + 77 + iclogo + 5681d2fc-c8ff-11ed-9255-dbd0817dbade + enhmemsy.png + image/png + 1403 + png + + 2023-03-22 23:17:31 + + + 92 + 78 + iclogo + 5681f570-c8ff-11ed-a813-f668082c0eab + ensoniq.png + image/png + 3557 + png + + 2023-03-22 23:17:31 + + + 93 + 79 + iclogo + 56821992-c8ff-11ed-91e9-cb9f497faf3e + eon.png + image/png + 5393 + png + + 2023-03-22 23:17:31 + + + 94 + 80 + iclogo + 56823eea-c8ff-11ed-8258-778fb9c27989 + epson1.png + image/png + 2349 + png + + 2023-03-22 23:17:31 + + + 95 + 80 + iclogo + 56824aa2-c8ff-11ed-a738-8dd2b09d2cb0 + epson2.png + image/png + 2405 + png + + 2023-03-22 23:17:31 + + + 96 + 81 + iclogo + 568279aa-c8ff-11ed-b1e9-b3441cd92dcb + ericsson.png + image/png + 4184 + png + + 2023-03-22 23:17:31 + + + 97 + 82 + iclogo + 5682a984-c8ff-11ed-b137-4ec668ffac51 + ess.png + image/png + 3030 + png + + 2023-03-22 23:17:31 + + + 98 + 83 + iclogo + 5682d7ce-c8ff-11ed-9bdf-fba8f1d0ec89 + etc.png + image/png + 2189 + png + + 2023-03-22 23:17:31 + + + 99 + 84 + iclogo + 5683046a-c8ff-11ed-8190-aebb6953623c + exar.png + image/png + 2771 + png + + 2023-03-22 23:17:31 + + + 100 + 85 + iclogo + 56833232-c8ff-11ed-b3a7-2060823541e9 + excelsemi1.png + image/png + 7632 + png + + 2023-03-22 23:17:31 + + + 101 + 85 + iclogo + 56833da4-c8ff-11ed-9e27-7f7898b11cf8 + excelsemi2.png + image/png + 2339 + png + + 2023-03-22 23:17:31 + + + 102 + 85 + iclogo + 5683488a-c8ff-11ed-99a6-dbbb65360908 + exel.png + image/png + 2771 + png + + 2023-03-22 23:17:31 + + + 103 + 86 + iclogo + 56836aea-c8ff-11ed-bd24-4cbbf7fc3d24 + fairchil.png + image/png + 1552 + png + + 2023-03-22 23:17:31 + + + 104 + 87 + iclogo + 56838f0c-c8ff-11ed-ad0a-62c536c263e8 + freescale.png + image/png + 3840 + png + + 2023-03-22 23:17:31 + + + 105 + 88 + iclogo + 5683b108-c8ff-11ed-a0fe-21e98a327412 + fujielec.png + image/png + 5048 + png + + 2023-03-22 23:17:31 + + + 106 + 88 + iclogo + 5683bbe4-c8ff-11ed-a164-bb108f090d77 + fujitsu2.png + image/png + 1860 + png + + 2023-03-22 23:17:31 + + + 107 + 89 + iclogo + 5683e060-c8ff-11ed-9d97-e097c8a106fd + galileo.png + image/png + 3779 + png + + 2023-03-22 23:17:31 + + + 108 + 90 + iclogo + 568402d4-c8ff-11ed-b834-4e88d7741e15 + galvant.png + image/png + 2669 + png + + 2023-03-22 23:17:31 + + + 109 + 91 + iclogo + 56842638-c8ff-11ed-95ac-73b2d4057ca6 + gecples.png + image/png + 2312 + png + + 2023-03-22 23:17:31 + + + 110 + 92 + iclogo + 56848f92-c8ff-11ed-999c-6f752c409f65 + gennum.png + image/png + 2614 + png + + 2023-03-22 23:17:31 + + + 111 + 93 + iclogo + 5684b9d6-c8ff-11ed-8e78-5217bce7d59a + ge.png + image/png + 2321 + png + + 2023-03-22 23:17:31 + + + 112 + 94 + iclogo + 5684e1ea-c8ff-11ed-80bc-a1b40bb97c23 + gi1.png + image/png + 1385 + png + + 2023-03-22 23:17:31 + + + 113 + 94 + iclogo + 5684eca8-c8ff-11ed-a7fe-b2538daeb6f7 + gi.png + image/png + 1691 + png + + 2023-03-22 23:17:31 + + + 114 + 95 + iclogo + 568511ba-c8ff-11ed-9986-8acdf1e13ae5 + glink.png + image/png + 1706 + png + + 2023-03-22 23:17:31 + + + 115 + 96 + iclogo + 56853abe-c8ff-11ed-b19f-b21d77131e35 + goal1.png + image/png + 9092 + png + + 2023-03-22 23:17:31 + + + 116 + 96 + iclogo + 56854766-c8ff-11ed-a12b-77f2a2c8cc6b + goal2.png + image/png + 9649 + png + + 2023-03-22 23:17:31 + + + 117 + 97 + iclogo + 56856c5a-c8ff-11ed-974f-953d09d40956 + goldstar1.png + image/png + 2923 + png + + 2023-03-22 23:17:31 + + + 118 + 97 + iclogo + 568577fe-c8ff-11ed-9958-083abcd834b1 + goldstar2.png + image/png + 11387 + png + + 2023-03-22 23:17:31 + + + 119 + 98 + iclogo + 56859d74-c8ff-11ed-a155-dd29dab1a09c + gould.png + image/png + 1549 + png + + 2023-03-22 23:17:31 + + + 120 + 99 + iclogo + 5685c164-c8ff-11ed-8dba-fa4e2521e98c + greenwich.png + image/png + 9761 + png + + 2023-03-22 23:17:31 + + + 121 + 100 + iclogo + 5685e5ae-c8ff-11ed-ab49-4ae5feb9550d + gsemi.png + image/png + 1704 + png + + 2023-03-22 23:17:31 + + + 122 + 101 + iclogo + 56860a2a-c8ff-11ed-a645-a6c651179b34 + harris1.png + image/png + 1549 + png + + 2023-03-22 23:17:31 + + + 123 + 101 + iclogo + 568615e2-c8ff-11ed-9f83-08d670bcbb46 + harris2.png + image/png + 1874 + png + + 2023-03-22 23:17:31 + + + 124 + 102 + iclogo + 56863856-c8ff-11ed-9510-9409bd91334e + hfo.png + image/png + 1958 + png + + 2023-03-22 23:17:31 + + + 125 + 103 + iclogo + 56866f4c-c8ff-11ed-8460-cbfd6ff948f0 + hitachi.png + image/png + 2611 + png + + 2023-03-22 23:17:31 + + + 126 + 104 + iclogo + 56869418-c8ff-11ed-b2ca-b0fc0d7dec8c + holtek.png + image/png + 2160 + png + + 2023-03-22 23:17:31 + + + 127 + 105 + iclogo + 5686baa6-c8ff-11ed-8413-6b45ea7860c4 + hp.png + image/png + 2464 + png + + 2023-03-22 23:17:31 + + + 128 + 106 + iclogo + 5686e044-c8ff-11ed-8e4f-18fe52f91fd4 + hualon.png + image/png + 2864 + png + + 2023-03-22 23:17:31 + + + 129 + 107 + iclogo + 568702cc-c8ff-11ed-8eb3-2427c2a58056 + hynix.png + image/png + 8444 + png + + 2023-03-22 23:17:31 + + + 130 + 108 + iclogo + 56872ee6-c8ff-11ed-8c68-27075e0b74fe + hyundai2.png + image/png + 2269 + png + + 2023-03-22 23:17:31 + + + 131 + 109 + iclogo + 56875380-c8ff-11ed-997e-cb42d307c52b + icdesign.png + image/png + 3014 + png + + 2023-03-22 23:17:31 + + + 132 + 110 + iclogo + 56877860-c8ff-11ed-b541-88b27e33d0e8 + icd.png + image/png + 1641 + png + + 2023-03-22 23:17:31 + + + 133 + 110 + iclogo + 568782ec-c8ff-11ed-8bcb-1a6fbffe8d9a + ics.png + image/png + 2042 + png + + 2023-03-22 23:17:31 + + + 134 + 111 + iclogo + 5687aa42-c8ff-11ed-a4cf-cf5827b5d295 + ichaus1.png + image/png + 3370 + png + + 2023-03-22 23:17:31 + + + 135 + 111 + iclogo + 5687b51e-c8ff-11ed-ae42-654971fe0b86 + ichaus.png + image/png + 1552 + png + + 2023-03-22 23:17:31 + + + 136 + 112 + iclogo + 5687d7ec-c8ff-11ed-b867-bf966a38be76 + icsi.png + image/png + 4049 + png + + 2023-03-22 23:17:31 + + + 137 + 113 + iclogo + 5687ff10-c8ff-11ed-806e-0ac441837823 + icube.png + image/png + 1629 + png + + 2023-03-22 23:17:31 + + + 138 + 114 + iclogo + 56882832-c8ff-11ed-85ef-218e037c424b + icworks.png + image/png + 1874 + png + + 2023-03-22 23:17:31 + + + 139 + 115 + iclogo + 56884e20-c8ff-11ed-8f03-8bc418e54276 + idt1.png + image/png + 3995 + png + + 2023-03-22 23:17:31 + + + 140 + 115 + iclogo + 5688587a-c8ff-11ed-896a-6eb01b1e8b6a + idt.png + image/png + 1553 + png + + 2023-03-22 23:17:31 + + + 141 + 116 + iclogo + 56887c88-c8ff-11ed-b382-388c27f86648 + igstech.png + image/png + 3832 + png + + 2023-03-22 23:17:31 + + + 142 + 117 + iclogo + 5688a3b6-c8ff-11ed-958a-57bcf30f0e8e + impala.png + image/png + 1628 + png + + 2023-03-22 23:17:31 + + + 143 + 118 + iclogo + 5688c95e-c8ff-11ed-974c-2becd8d95cfe + imp.png + image/png + 2175 + png + + 2023-03-22 23:17:31 + + + 144 + 119 + iclogo + 5688efce-c8ff-11ed-bd6a-3a3f23feed22 + infineon.png + image/png + 4511 + png + + 2023-03-22 23:17:31 + + + 145 + 120 + iclogo + 568915c6-c8ff-11ed-9828-99f8f9466667 + inmos.png + image/png + 3365 + png + + 2023-03-22 23:17:31 + + + 146 + 121 + iclogo + 56893c18-c8ff-11ed-addc-4d17b44d41a9 + intel2.png + image/png + 2010 + png + + 2023-03-22 23:17:31 + + + 147 + 122 + iclogo + 56896026-c8ff-11ed-8bb4-690773d704b3 + intresil4.png + image/png + 2614 + png + + 2023-03-22 23:17:31 + + + 148 + 122 + iclogo + 56896b20-c8ff-11ed-a162-fc27a1f080ee + intrsil1.png + image/png + 1874 + png + + 2023-03-22 23:17:31 + + + 149 + 122 + iclogo + 5689757a-c8ff-11ed-a642-c1ff2b88b16d + intrsil2.png + image/png + 2520 + png + + 2023-03-22 23:17:31 + + + 150 + 122 + iclogo + 56897fd4-c8ff-11ed-8b61-ecdd2f4375cf + intrsil3.png + image/png + 3295 + png + + 2023-03-22 23:17:31 + + + 151 + 123 + iclogo + 5689a914-c8ff-11ed-8471-671c4d3526a2 + ir.png + image/png + 2729 + png + + 2023-03-22 23:17:31 + + + 152 + 124 + iclogo + 5689cdae-c8ff-11ed-8640-2c6e9bf8b270 + isd.png + image/png + 2554 + png + + 2023-03-22 23:17:31 + + + 153 + 125 + iclogo + 5689f3ec-c8ff-11ed-962a-2fc58c1f358f + issi.png + image/png + 3030 + png + + 2023-03-22 23:17:31 + + + 154 + 126 + iclogo + 568a1912-c8ff-11ed-bd7e-d179c93e6b00 + ite.png + image/png + 3302 + png + + 2023-03-22 23:17:31 + + + 155 + 127 + iclogo + 568a3dc0-c8ff-11ed-8f3d-6c1dd312337c + itt.png + image/png + 2483 + png + + 2023-03-22 23:17:31 + + + 156 + 128 + iclogo + 568a6228-c8ff-11ed-b9c8-00902f96aeb6 + ixys.png + image/png + 3575 + png + + 2023-03-22 23:17:31 + + + 157 + 129 + iclogo + 568a85d2-c8ff-11ed-b548-8f4b43deb58f + kec.png + image/png + 2567 + png + + 2023-03-22 23:17:31 + + + 158 + 130 + iclogo + 568aa7e2-c8ff-11ed-ab2f-b53ec050a242 + kota.png + image/png + 1552 + png + + 2023-03-22 23:17:31 + + + 159 + 131 + iclogo + 568acb82-c8ff-11ed-aebb-42187109e347 + lattice1.png + image/png + 1768 + png + + 2023-03-22 23:17:31 + + + 160 + 131 + iclogo + 568ad58c-c8ff-11ed-aa8d-592ab1c33b53 + lattice2.png + image/png + 1519 + png + + 2023-03-22 23:17:31 + + + 161 + 131 + iclogo + 568adea6-c8ff-11ed-8ff6-d407a412924f + lattice3.png + image/png + 1216 + png + + 2023-03-22 23:17:31 + + + 162 + 132 + iclogo + 568b025a-c8ff-11ed-8b03-92c6d544d1f4 + lds1.png + image/png + 2136 + png + + 2023-03-22 23:17:31 + + + 163 + 132 + iclogo + 568b0c82-c8ff-11ed-91b1-cf8307f47c99 + lds.png + image/png + 1959 + png + + 2023-03-22 23:17:31 + + + 164 + 133 + iclogo + 568b2e42-c8ff-11ed-98f1-c29c79404afe + levone.png + image/png + 4189 + png + + 2023-03-22 23:17:31 + + + 165 + 134 + iclogo + 568b544e-c8ff-11ed-8b7a-50043605a2b6 + lgs1.png + image/png + 2417 + png + + 2023-03-22 23:17:31 + + + 166 + 134 + iclogo + 568b5f0c-c8ff-11ed-a373-1898c7e0a30c + lgs.png + image/png + 737 + png + + 2023-03-22 23:17:31 + + + 167 + 135 + iclogo + 568b81ee-c8ff-11ed-a86f-8114cf642140 + linear.png + image/png + 2486 + png + + 2023-03-22 23:17:31 + + + 168 + 136 + iclogo + 568ba5ac-c8ff-11ed-b29d-165934da4046 + linfin.png + image/png + 4844 + png + + 2023-03-22 23:17:31 + + + 169 + 137 + iclogo + 568bc9e2-c8ff-11ed-a3f0-c10bfee4f5a5 + liteon.png + image/png + 2388 + png + + 2023-03-22 23:17:31 + + + 170 + 138 + iclogo + 568beeea-c8ff-11ed-a2f1-7df5051d824f + lucent.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 171 + 139 + iclogo + 568c112c-c8ff-11ed-9884-378bfa8cd82f + macronix.png + image/png + 2324 + png + + 2023-03-22 23:17:31 + + + 172 + 140 + iclogo + 568c354e-c8ff-11ed-9ded-8e10fddeba15 + marvell.png + image/png + 3131 + png + + 2023-03-22 23:17:31 + + + 173 + 141 + iclogo + 568c592a-c8ff-11ed-b6a8-887699641615 + matsush1.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 174 + 141 + iclogo + 568c644c-c8ff-11ed-bdf5-a1555770b909 + matsushi.png + image/png + 2029 + png + + 2023-03-22 23:17:31 + + + 175 + 142 + iclogo + 568c881e-c8ff-11ed-aa7c-fa4001861926 + maxim.png + image/png + 2690 + png + + 2023-03-22 23:17:31 + + + 176 + 143 + iclogo + 568cac5e-c8ff-11ed-8b66-7e90a640da46 + mediavi1.png + image/png + 2189 + png + + 2023-03-22 23:17:31 + + + 177 + 143 + iclogo + 568cb604-c8ff-11ed-8003-6edfe1893ad6 + mediavi2.png + image/png + 2487 + png + + 2023-03-22 23:17:31 + + + 178 + 144 + iclogo + 568cd8b4-c8ff-11ed-8b6a-9380b4a1402f + me.png + image/png + 2411 + png + + 2023-03-22 23:17:31 + + + 179 + 144 + iclogo + 568ce3c2-c8ff-11ed-8aee-b8f55cd58993 + microchp.png + image/png + 2814 + png + + 2023-03-22 23:17:31 + + + 180 + 145 + iclogo + 568d0758-c8ff-11ed-a08d-98ede1f64041 + mhs2.png + image/png + 2036 + png + + 2023-03-22 23:17:31 + + + 181 + 145 + iclogo + 568d12ac-c8ff-11ed-92de-bed578696e3c + mhs.png + image/png + 1870 + png + + 2023-03-22 23:17:31 + + + 182 + 146 + iclogo + 568d3638-c8ff-11ed-9213-ac7f3ed2ffda + micrel1.png + image/png + 9695 + png + + 2023-03-22 23:17:31 + + + 183 + 146 + iclogo + 568d4268-c8ff-11ed-8860-a64981c73948 + micrel2.png + image/png + 9695 + png + + 2023-03-22 23:17:31 + + + 184 + 147 + iclogo + 568d67ac-c8ff-11ed-93b0-302d67844227 + micronas.png + image/png + 1871 + png + + 2023-03-22 23:17:31 + + + 185 + 148 + iclogo + 568d8f34-c8ff-11ed-8c81-056ec8ba5c17 + micronix.png + image/png + 1856 + png + + 2023-03-22 23:17:31 + + + 186 + 149 + iclogo + 568db1b2-c8ff-11ed-8e67-e47603874a4b + micron.png + image/png + 1763 + png + + 2023-03-22 23:17:31 + + + 187 + 150 + iclogo + 568dd5ca-c8ff-11ed-9106-d633745d51c5 + microsemi1.png + image/png + 3714 + png + + 2023-03-22 23:17:31 + + + 188 + 150 + iclogo + 568de0ce-c8ff-11ed-859a-9f73181a3e5c + microsemi2.png + image/png + 11992 + png + + 2023-03-22 23:17:31 + + + 189 + 151 + iclogo + 568e0568-c8ff-11ed-8bd2-bd4a8fea36d6 + minicirc.png + image/png + 1391 + png + + 2023-03-22 23:17:31 + + + 190 + 152 + iclogo + 568e2a0c-c8ff-11ed-9b90-ff9eb99be150 + mitel.png + image/png + 2819 + png + + 2023-03-22 23:17:31 + + + 191 + 153 + iclogo + 568e4d5c-c8ff-11ed-8622-0e50f183e8fa + mitsubis.png + image/png + 2311 + png + + 2023-03-22 23:17:31 + + + 192 + 154 + iclogo + 568e72f0-c8ff-11ed-8568-09f01a3a2670 + mlinear.png + image/png + 3377 + png + + 2023-03-22 23:17:31 + + + 193 + 155 + iclogo + 568e96f4-c8ff-11ed-86ea-8d3a071d766d + mmi.png + image/png + 2692 + png + + 2023-03-22 23:17:31 + + + 194 + 156 + iclogo + 568ebbac-c8ff-11ed-a4d7-8eca7f888ba4 + mosaic.png + image/png + 2959 + png + + 2023-03-22 23:17:31 + + + 195 + 157 + iclogo + 568ee064-c8ff-11ed-90d5-084340453793 + moselvit.png + image/png + 2504 + png + + 2023-03-22 23:17:31 + + + 196 + 158 + iclogo + 568f0594-c8ff-11ed-a5e8-df73bc079b7c + mos.png + image/png + 2857 + png + + 2023-03-22 23:17:31 + + + 197 + 159 + iclogo + 568f37d0-c8ff-11ed-80d1-a7dd8dce594b + mostek1.png + image/png + 7502 + png + + 2023-03-22 23:17:31 + + + 198 + 159 + iclogo + 568f43ba-c8ff-11ed-8a49-a8e9c5b40903 + mostek2.png + image/png + 7502 + png + + 2023-03-22 23:17:31 + + + 199 + 159 + iclogo + 568f4ec8-c8ff-11ed-bc4c-848264d621d5 + mostek3.png + image/png + 2514 + png + + 2023-03-22 23:17:31 + + + 200 + 160 + iclogo + 568f7718-c8ff-11ed-b5cb-fec4c4f0de0e + mosys.png + image/png + 2321 + png + + 2023-03-22 23:17:31 + + + 201 + 161 + iclogo + 568f9b3a-c8ff-11ed-a51e-a346b463cb1f + motorol1.png + image/png + 999 + png + + 2023-03-22 23:17:31 + + + 202 + 161 + iclogo + 568fa616-c8ff-11ed-aa80-42f6c9e69760 + motorol2.png + image/png + 2417 + png + + 2023-03-22 23:17:31 + + + 203 + 162 + iclogo + 568fc970-c8ff-11ed-a093-161f51674dcb + mpd.png + image/png + 2663 + png + + 2023-03-22 23:17:31 + + + 204 + 163 + iclogo + 568fefae-c8ff-11ed-9d67-64959a0c66d9 + msystem.png + image/png + 1670 + png + + 2023-03-22 23:17:31 + + + 205 + 164 + iclogo + 569012a4-c8ff-11ed-8fe4-04a628989fd4 + murata1.png + image/png + 4874 + png + + 2023-03-22 23:17:31 + + + 206 + 164 + iclogo + 56901e48-c8ff-11ed-a434-cfdc4d826d34 + murata.png + image/png + 4777 + png + + 2023-03-22 23:17:31 + + + 207 + 165 + iclogo + 56904422-c8ff-11ed-abc1-98d52541c298 + mwave.png + image/png + 3370 + png + + 2023-03-22 23:17:31 + + + 208 + 166 + iclogo + 56906b50-c8ff-11ed-a39a-db0fbab6e9d0 + myson.png + image/png + 1932 + png + + 2023-03-22 23:17:31 + + + 209 + 167 + iclogo + 5690901c-c8ff-11ed-b105-9611ce907bee + nec1.png + image/png + 3166 + png + + 2023-03-22 23:17:31 + + + 210 + 167 + iclogo + 56909ad0-c8ff-11ed-9ef5-3fd91a35cadc + nec2.png + image/png + 3071 + png + + 2023-03-22 23:17:31 + + + 211 + 168 + iclogo + 5690bc5e-c8ff-11ed-bc71-4eda4a9c6182 + nexflash.png + image/png + 7789 + png + + 2023-03-22 23:17:31 + + + 212 + 169 + iclogo + 5690e1a2-c8ff-11ed-b344-ae7ef2663dd1 + njr.png + image/png + 3419 + png + + 2023-03-22 23:17:31 + + + 213 + 170 + iclogo + 569105f6-c8ff-11ed-9ace-8448b0d43dc7 + ns1.png + image/png + 1959 + png + + 2023-03-22 23:17:31 + + + 214 + 170 + iclogo + 5691103c-c8ff-11ed-88d0-4889d374d080 + ns2.png + image/png + 1952 + png + + 2023-03-22 23:17:31 + + + 215 + 171 + iclogo + 569134b8-c8ff-11ed-9371-8171479e9467 + nvidia.png + image/png + 1874 + png + + 2023-03-22 23:17:31 + + + 216 + 172 + iclogo + 56915812-c8ff-11ed-a88a-1ee3e984ee1e + oak.png + image/png + 2614 + png + + 2023-03-22 23:17:31 + + + 217 + 173 + iclogo + 56917be4-c8ff-11ed-945d-d0d25bc0c57a + oki1.png + image/png + 2267 + png + + 2023-03-22 23:17:31 + + + 218 + 173 + iclogo + 5691862a-c8ff-11ed-8300-2b2dbc7b9070 + oki.png + image/png + 2546 + png + + 2023-03-22 23:17:31 + + + 219 + 174 + iclogo + 5691a98e-c8ff-11ed-ab20-b034f44ad412 + opti.png + image/png + 1684 + png + + 2023-03-22 23:17:31 + + + 220 + 175 + iclogo + 5691cd24-c8ff-11ed-a148-64e44275e7c9 + orbit.png + image/png + 3347 + png + + 2023-03-22 23:17:31 + + + 221 + 176 + iclogo + 5691f10a-c8ff-11ed-a8aa-c6598f17b11f + oren.png + image/png + 3497 + png + + 2023-03-22 23:17:31 + + + 222 + 177 + iclogo + 5692134c-c8ff-11ed-9646-44382c2039c7 + perform.png + image/png + 3284 + png + + 2023-03-22 23:17:31 + + + 223 + 178 + iclogo + 56923796-c8ff-11ed-88e2-75a8035a68cd + pericom.png + image/png + 2311 + png + + 2023-03-22 23:17:31 + + + 224 + 179 + iclogo + 56925bf4-c8ff-11ed-b290-e6e40eef9273 + phaslink.png + image/png + 2669 + png + + 2023-03-22 23:17:31 + + + 225 + 180 + iclogo + 569280ca-c8ff-11ed-9f14-9152a84119d5 + philips.png + image/png + 8690 + png + + 2023-03-22 23:17:31 + + + 226 + 181 + iclogo + 5692ae92-c8ff-11ed-ade5-83e922a02acc + plx.png + image/png + 4749 + png + + 2023-03-22 23:17:31 + + + 227 + 182 + iclogo + 5692d1c4-c8ff-11ed-bf7d-6fbac519872c + pmc.png + image/png + 3497 + png + + 2023-03-22 23:17:31 + + + 228 + 183 + iclogo + 5692f686-c8ff-11ed-9d7b-6564d4e66103 + pmi.png + image/png + 3807 + png + + 2023-03-22 23:17:31 + + + 229 + 184 + iclogo + 56931dc8-c8ff-11ed-95c1-4c6f225a64e4 + ptc.png + image/png + 2669 + png + + 2023-03-22 23:17:31 + + + 230 + 185 + iclogo + 569342b2-c8ff-11ed-a1ce-a1013da9bf84 + pwrsmart.png + image/png + 1389 + png + + 2023-03-22 23:17:31 + + + 231 + 186 + iclogo + 5693679c-c8ff-11ed-9863-94fd3160386d + qlogic.png + image/png + 1709 + png + + 2023-03-22 23:17:31 + + + 232 + 187 + iclogo + 56938b96-c8ff-11ed-92a8-2ba0a126c643 + qualcomm.png + image/png + 3326 + png + + 2023-03-22 23:17:31 + + + 233 + 188 + iclogo + 5693b076-c8ff-11ed-8e1a-21c0bab343d6 + quality.png + image/png + 1309 + png + + 2023-03-22 23:17:31 + + + 234 + 189 + iclogo + 5693d600-c8ff-11ed-bcce-4a2083ba53fe + rabbit.png + image/png + 2857 + png + + 2023-03-22 23:17:31 + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + 3 + Max Current + > + + + + numeric + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 7 + 3 + 100k Ohm + Resistor 100 + Test + 0 + 10 + 0.0000 + Active + 1 + Test + Hallo + 2023-03-22 23:38:06 + IPN1 + 0 + 1 + 0 + 1 + 1 + + + 2 + 5 + + Test + + + 0 + 0 + 0.0000 + + 0 + + + 2023-03-22 23:40:07 + + 0 + 0 + 1 + 1 + + + + 3 + 6 + + Test + + + 50 + 0 + 0.0000 + + 0 + + + 2023-03-22 23:40:40 + + 0 + 0 + 0 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + 1 + 1 + PartAttachment + 75d71f60-c902-11ed-8740-1bd85445aac2 + Lückenauskunft.pdf + application/pdf + 7763 + + + 2023-03-22 23:39:52 + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + 14 + 0 + 1 + Root Category + + Root Category + + + 2 + 1 + 2 + 9 + 1 + 1 + Active Parts + + Root Category ➤ Active Parts + + + 3 + 1 + 10 + 13 + 1 + 1 + Passive Parts + + Root Category ➤ Passive Parts + + + 4 + 2 + 3 + 8 + 2 + 1 + Transistors + + Root Category ➤ Active Parts ➤ Transistors + + + 5 + 4 + 4 + 5 + 3 + 1 + NPN + + Root Category ➤ Active Parts ➤ Transistors ➤ NPN + + + 6 + 4 + 6 + 7 + 3 + 1 + PNP + + Root Category ➤ Active Parts ➤ Transistors ➤ PNP + + + 7 + 3 + 11 + 12 + 2 + 1 + Resistors + + Root Category ➤ Passive Parts ➤ Resistors + + + + + + + + + + + + + + + + + + + + 1 + 1 + 1 + BC547 + 2 + 2.4000 + + + + + + 2 + 1 + 1 + BC547 + 1 + 1.0000 + + Test + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + partkeepr + + + 0 + 0 + + 1 + 0 + + + 2 + 1 + partkeepr2 + + + 0 + 0 + + 1 + 0 + + + + + + + + + + + + + + + 1 + 1 + 5 + SDFKds + + + 2 + 1 + 5 + Marcoos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + 3 + Max Current + Test + 1 + 1 + 23 + 23 + 10 + 10 + + numeric + + + + + + + + + + + + + + + + 1 + Pieces + pcs + 1 + + + 2 + Meter + m + 0 + + + + + + + + + + + + + + 1 + + Test Project + Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + 1 + Test + absolute + 0 + Test234 + + + 2 + + 1 + 10 + PCB + absolute + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20150608120000 + + + 20150708120022 + + + 20150724174030 + + + 20151001180120 + + + 20151002183125 + + + 20151031163951 + + + 20151208162723 + + + 20160103145302 + + + 20170108122512 + + + 20170108143802 + + + 20170113203042 + + + 20170601175559 + + + + + + + + + + + + + + 1 + yotta + Y + 24 + 10 + + + 2 + zetta + Z + 21 + 10 + + + 3 + exa + E + 18 + 10 + + + 4 + peta + P + 15 + 10 + + + 5 + tera + T + 12 + 10 + + + 6 + giga + G + 9 + 10 + + + 7 + mega + M + 6 + 10 + + + 8 + kilo + k + 3 + 10 + + + 9 + hecto + h + 2 + 10 + + + 10 + deca + da + 1 + 10 + + + 11 + - + + 0 + 10 + + + 12 + deci + d + -1 + 10 + + + 13 + centi + c + -2 + 10 + + + 14 + milli + m + -3 + 10 + + + 15 + micro + μ + -6 + 10 + + + 16 + nano + n + -9 + 10 + + + 17 + pico + p + -12 + 10 + + + 18 + femto + f + -15 + 10 + + + 19 + atto + a + -18 + 10 + + + 20 + zepto + z + -21 + 10 + + + 21 + yocto + y + -24 + 10 + + + 22 + kibi + Ki + 1 + 1024 + + + 23 + mebi + Mi + 2 + 1024 + + + 24 + gibi + Gi + 3 + 1024 + + + 25 + tebi + Ti + 4 + 1024 + + + 26 + pebi + Pi + 5 + 1024 + + + 27 + exbi + Ei + 6 + 1024 + + + 28 + zebi + Zi + 7 + 1024 + + + 29 + yobi + Yi + 8 + 1024 + + + + + + + + + + + + + 1 + 2023-03-22 23:27:34 + 0 + 1 + + + 2 + 2023-03-22 23:28:48 + 0 + 1 + + + + + + + + + + + + + + + 1 + 0 + 1 + 1 + + + 2 + 0 + 2 + 1 + + + + + + + + + + + + + + + + + + + 1 + 3 + + 50 + 0.0000 + 2023-03-22 23:40:40 + 0 + + + + + + + + + + + + + + + 1 + 1 + Location 1 + + + 2 + 1 + Location 2 + + + 3 + 8 + Sublocation 1 + + + 4 + 8 + Sublocation 2 + + + 5 + 9 + Hallo 2 + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + 6 + 0 + 1 + Root Category + + Root Category + + + 8 + 1 + 2 + 5 + 1 + 1 + Test + + Root Category ➤ Test + + + 9 + 8 + 3 + 4 + 2 + 1 + Level 2 + + Root Category ➤ Test ➤ Level 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + tempfile + 9cd27bec-c901-11ed-ba1f-d163ec6b8aa1 + export (1).csv + text/plain + 12 + + + 2023-03-22 23:33:48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + Meter + m + + + 2 + Gram + g + + + 3 + Second + s + + + 4 + Kelvin + K + + + 5 + Mol + mol + + + 6 + Candela + cd + + + 7 + Ampere + A + + + 8 + Ohm + Ω + + + 9 + Volt + V + + + 10 + Hertz + Hz + + + 11 + Newton + N + + + 12 + Pascal + Pa + + + 13 + Joule + J + + + 14 + Watt + W + + + 15 + Coulomb + C + + + 16 + Farad + F + + + 17 + Siemens + S + + + 18 + Weber + Wb + + + 19 + Tesla + T + + + 20 + Henry + H + + + 21 + Celsius + °C + + + 22 + Lumen + lm + + + 23 + Lux + lx + + + 24 + Becquerel + Bq + + + 25 + Gray + Gy + + + 26 + Sievert + Sv + + + 27 + Katal + kat + + + 28 + Ampere Hour + Ah + + + + + + + + + + + + + + 1 + 8 + + + 1 + 11 + + + 1 + 12 + + + 1 + 13 + + + 1 + 14 + + + 1 + 15 + + + 1 + 16 + + + 2 + 8 + + + 2 + 11 + + + 2 + 14 + + + 3 + 11 + + + 3 + 14 + + + 4 + 11 + + + 5 + 11 + + + 6 + 14 + + + 7 + 8 + + + 7 + 11 + + + 7 + 14 + + + 7 + 15 + + + 7 + 16 + + + 7 + 17 + + + 8 + 5 + + + 8 + 6 + + + 8 + 8 + + + 8 + 11 + + + 8 + 14 + + + 8 + 15 + + + 9 + 8 + + + 9 + 11 + + + 9 + 14 + + + 10 + 5 + + + 10 + 6 + + + 10 + 8 + + + 10 + 11 + + + 10 + 14 + + + 11 + 8 + + + 11 + 11 + + + 12 + 8 + + + 12 + 11 + + + 12 + 14 + + + 13 + 8 + + + 13 + 11 + + + 13 + 14 + + + 13 + 15 + + + 14 + 6 + + + 14 + 7 + + + 14 + 8 + + + 14 + 11 + + + 14 + 14 + + + 14 + 15 + + + 15 + 8 + + + 15 + 11 + + + 16 + 11 + + + 16 + 14 + + + 16 + 15 + + + 16 + 16 + + + 16 + 17 + + + 17 + 11 + + + 17 + 14 + + + 18 + 11 + + + 19 + 11 + + + 20 + 11 + + + 20 + 14 + + + 20 + 15 + + + 21 + 11 + + + 22 + 11 + + + 23 + 11 + + + 24 + 11 + + + 25 + 11 + + + 26 + 11 + + + 26 + 14 + + + 26 + 15 + + + 27 + 11 + + + 28 + 8 + + + 28 + 11 + + + 28 + 14 + + + + + + + + + + + + + + 1 + partkeepr.tipoftheday.showtips + s:4:"true"; + + + + + + + + + + + + + 1 + Builtin + 1 + + + + From 34aefd32e841ba79ad3bf0a44f833b976019db4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Mar 2023 22:41:33 +0100 Subject: [PATCH 02/12] Added possibility to import categories and footprints --- .../Migrations/ImportPartKeeprCommand.php | 8 + .../ImportExportSystem/PartkeeprImporter.php | 138 +++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index d860ce03..829dfbd0 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -91,6 +91,14 @@ class ImportPartKeeprCommand extends Command $io->info('Importing manufacturers...'); $count = $this->importer->importManufacturers($data); $io->success('Imported '.$count.' manufacturers.'); + + $io->info('Importing categories...'); + $count = $this->importer->importCategories($data); + $io->success('Imported '.$count.' categories.'); + + $io->info('Importing Footprints...'); + $count = $this->importer->importFootprints($data); + $io->success('Imported '.$count.' footprints.'); } } \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php index d2bf13f7..5887b56c 100644 --- a/src/Services/ImportExportSystem/PartkeeprImporter.php +++ b/src/Services/ImportExportSystem/PartkeeprImporter.php @@ -23,6 +23,9 @@ namespace App\Services\ImportExportSystem; use App\Doctrine\Purger\ResetAutoIncrementORMPurger; use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory; use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; @@ -144,8 +147,141 @@ class PartkeeprImporter return count($partunit_data); } - public function setIDOfEntity(AbstractDBElement $element, int $id): void + public function importCategories(array $data): int { + if (!isset($data['partcategory'])) { + throw new \RuntimeException('$data must contain a "partcategory" key!'); + } + + $partcategory_data = $data['partcategory']; + + //In a first step, create all categories like they were a flat structure (so ignore the parent) + foreach ($partcategory_data as $partcategory) { + $category = new Category(); + $category->setName($partcategory['name']); + $category->setComment($partcategory['description']); + + $this->setIDOfEntity($category, $partcategory['id']); + $this->em->persist($category); + } + + $this->em->flush(); + + //In a second step, set the correct parent element + foreach ($partcategory_data as $partcategory) { + $this->setParent(Category::class, $partcategory['id'], $partcategory['parent_id']); + } + $this->em->flush(); + + return count($partcategory_data); + } + + private function importElementsWithCategory(array $data, string $target_class, string $data_prefix): int + { + + } + + public function importFootprints(array $data): int + { + if (!isset($data['footprint'])) { + throw new \RuntimeException('$data must contain a "footprint" key!'); + } + if (!isset($data['footprintcategory'])) { + throw new \RuntimeException('$data must contain a "footprintcategory" key!'); + } + + //We import the footprints first, as we need the IDs of the footprints be our real DBs later (as we match the part import by ID) + //As the footprints category is not existing yet, we just skip the parent field for now + $footprint_data = $data['footprint']; + $max_footprint_id = 0; + foreach ($footprint_data as $footprint) { + $entity = new Footprint(); + $entity->setName($footprint['name']); + $entity->setComment($footprint['description']); + + $this->setIDOfEntity($entity, $footprint['id']); + $this->em->persist($entity); + $max_footprint_id = max($max_footprint_id, (int) $footprint['id']); + } + + //Import the footprint categories ignoring the parents for now + //Their IDs are $max_footprint_id + $ID + $footprintcategory_data = $data['footprintcategory']; + foreach ($footprintcategory_data as $footprintcategory) { + $entity = new Footprint(); + $entity->setName($footprintcategory['name']); + $entity->setComment($footprintcategory['description']); + //Categories are not assignable to parts, so we set them to not selectable + $entity->setNotSelectable(true); + + $this->setIDOfEntity($entity, $max_footprint_id + (int) $footprintcategory['id']); + $this->em->persist($entity); + } + + $this->em->flush(); + + //Now we can correct the parents and category IDs of the parts + foreach ($footprintcategory_data as $footprintcategory) { + //We have to use the mapped IDs here, as the imported ID is not the effective ID + if ($footprintcategory['parent_id']) { + $this->setParent(Footprint::class, $max_footprint_id + (int)$footprintcategory['id'], + $max_footprint_id + (int)$footprintcategory['parent_id']); + } + } + foreach ($footprint_data as $footprint) { + if ($footprint['category_id']) { + $this->setParent(Footprint::class, $footprint['id'], + $max_footprint_id + (int)$footprint['category_id']); + } + } + + $this->em->flush(); + + return count($footprint_data) + count($footprintcategory_data); + } + + /** + * Assigns the parent to the given entity, using the numerical IDs from the imported data. + * @param string $class + * @param int|string $element_id + * @param int|string $parent_id + * @return AbstractStructuralDBElement The structural element that was modified (with $element_id) + */ + private function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement + { + $element = $this->em->find($class, (int) $element_id); + if (!$element) { + throw new \RuntimeException(sprintf('Could not find element with ID %s', $element_id)); + } + + //If the parent is null, we're done + if (!$parent_id) { + return $element; + } + + $parent = $this->em->find($class, (int) $parent_id); + if (!$parent) { + throw new \RuntimeException(sprintf('Could not find parent with ID %s', $parent_id)); + } + + $element->setParent($parent); + return $element; + } + + /** + * Set the ID of an entity to a specific value. Must be called before persisting the entity, but before flushing. + * @param AbstractDBElement $element + * @param int|string $id + * @return void + */ + public function setIDOfEntity(AbstractDBElement $element, $id): void + { + if (!is_int($id) && !is_string($id)) { + throw new \InvalidArgumentException('ID must be an integer or string'); + } + + $id = (int) $id; + $metadata = $this->em->getClassMetadata(get_class($element)); $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); From 1ca839ab2601e138400b478ba9a4258720a085f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Mar 2023 22:51:41 +0100 Subject: [PATCH 03/12] Added import for storelocations --- .../Migrations/ImportPartKeeprCommand.php | 4 ++ .../Base/AbstractStructuralDBElement.php | 4 +- .../ImportExportSystem/PartkeeprImporter.php | 46 +++++++++++++------ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index 829dfbd0..06185020 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -99,6 +99,10 @@ class ImportPartKeeprCommand extends Command $io->info('Importing Footprints...'); $count = $this->importer->importFootprints($data); $io->success('Imported '.$count.' footprints.'); + + $io->info('Importing storage locations...'); + $count = $this->importer->importStorelocations($data); + $io->success('Imported '.$count.' storage locations.'); } } \ No newline at end of file diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index 457d40c6..629669e9 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -340,11 +340,11 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /** * Set the comment. * - * @param string|null $new_comment the new comment + * @param string $new_comment the new comment * * @return AbstractStructuralDBElement */ - public function setComment(?string $new_comment): self + public function setComment(string $new_comment): self { $this->comment = $new_comment; diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php index 5887b56c..c112344a 100644 --- a/src/Services/ImportExportSystem/PartkeeprImporter.php +++ b/src/Services/ImportExportSystem/PartkeeprImporter.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory; use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; @@ -176,28 +177,33 @@ class PartkeeprImporter return count($partcategory_data); } + /** + * The common import functions for footprints and storeloactions + * @param array $data + * @param string $target_class + * @param string $data_prefix + * @return int + */ private function importElementsWithCategory(array $data, string $target_class, string $data_prefix): int { + $key = $data_prefix; + $category_key = $data_prefix.'category'; - } - - public function importFootprints(array $data): int - { - if (!isset($data['footprint'])) { - throw new \RuntimeException('$data must contain a "footprint" key!'); + if (!isset($data[$key])) { + throw new \RuntimeException('$data must contain a "'. $key .'" key!'); } - if (!isset($data['footprintcategory'])) { - throw new \RuntimeException('$data must contain a "footprintcategory" key!'); + if (!isset($data[$category_key])) { + throw new \RuntimeException('$data must contain a "'. $category_key .'" key!'); } //We import the footprints first, as we need the IDs of the footprints be our real DBs later (as we match the part import by ID) //As the footprints category is not existing yet, we just skip the parent field for now - $footprint_data = $data['footprint']; + $footprint_data = $data[$key]; $max_footprint_id = 0; foreach ($footprint_data as $footprint) { - $entity = new Footprint(); + $entity = new $target_class(); $entity->setName($footprint['name']); - $entity->setComment($footprint['description']); + $entity->setComment($footprint['description'] ?? ''); $this->setIDOfEntity($entity, $footprint['id']); $this->em->persist($entity); @@ -206,9 +212,9 @@ class PartkeeprImporter //Import the footprint categories ignoring the parents for now //Their IDs are $max_footprint_id + $ID - $footprintcategory_data = $data['footprintcategory']; + $footprintcategory_data = $data[$category_key]; foreach ($footprintcategory_data as $footprintcategory) { - $entity = new Footprint(); + $entity = new $target_class(); $entity->setName($footprintcategory['name']); $entity->setComment($footprintcategory['description']); //Categories are not assignable to parts, so we set them to not selectable @@ -224,13 +230,13 @@ class PartkeeprImporter foreach ($footprintcategory_data as $footprintcategory) { //We have to use the mapped IDs here, as the imported ID is not the effective ID if ($footprintcategory['parent_id']) { - $this->setParent(Footprint::class, $max_footprint_id + (int)$footprintcategory['id'], + $this->setParent($target_class, $max_footprint_id + (int)$footprintcategory['id'], $max_footprint_id + (int)$footprintcategory['parent_id']); } } foreach ($footprint_data as $footprint) { if ($footprint['category_id']) { - $this->setParent(Footprint::class, $footprint['id'], + $this->setParent($target_class, $footprint['id'], $max_footprint_id + (int)$footprint['category_id']); } } @@ -240,6 +246,16 @@ class PartkeeprImporter return count($footprint_data) + count($footprintcategory_data); } + public function importFootprints(array $data): int + { + return $this->importElementsWithCategory($data, Footprint::class, 'footprint'); + } + + public function importStorelocations(array $data): int + { + return $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); + } + /** * Assigns the parent to the given entity, using the numerical IDs from the imported data. * @param string $class From 21c74fbcc806864211e5ac1e49eeadde52c18d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Mar 2023 23:43:05 +0100 Subject: [PATCH 04/12] Added basic import for parts --- .../Migrations/ImportPartKeeprCommand.php | 4 + .../ImportExportSystem/PartkeeprImporter.php | 123 +++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index 06185020..87069389 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -103,6 +103,10 @@ class ImportPartKeeprCommand extends Command $io->info('Importing storage locations...'); $count = $this->importer->importStorelocations($data); $io->success('Imported '.$count.' storage locations.'); + + $io->info('Importing parts...'); + $count = $this->importer->importParts($data); + $io->success('Imported '.$count.' parts.'); } } \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php index c112344a..bb23b095 100644 --- a/src/Services/ImportExportSystem/PartkeeprImporter.php +++ b/src/Services/ImportExportSystem/PartkeeprImporter.php @@ -24,26 +24,32 @@ use App\Doctrine\Purger\ResetAutoIncrementORMPurger; use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\Parameters\PartParameter; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory; use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class PartkeeprImporter { protected EntityManagerInterface $em; + protected PropertyAccessorInterface $propertyAccessor; - public function __construct(EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) { $this->em = $em; + $this->propertyAccessor = $propertyAccessor; } public function purgeDatabaseForImport(): void @@ -256,6 +262,77 @@ class PartkeeprImporter return $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); } + public function importParts(array $data): int + { + if (!isset($data['part'])) { + throw new \RuntimeException('$data must contain a "part" key!'); + } + + + $part_data = $data['part']; + foreach ($part_data as $part) { + $entity = new Part(); + $entity->setName($part['name']); + $entity->setDescription($part['description'] ?? ''); + //All parts get a tag, that they were imported from PartKeepr + $entity->setTags('partkeepr-imported'); + + //If the part is a metapart, write that in the description, and we can skip the rest + if ($part['metaPart'] === '1') { + $entity->setDescription('Metapart (Not supported in Part-DB)'); + $entity->setComment('This part represents a former metapart in PartKeepr. It is not supported in Part-DB yet. And you can most likely delete it.'); + $entity->setTags('partkeepr-imported,partkeepr-metapart'); + } else { + $entity->setMinAmount($part['minStockLevel'] ?? 0); + if (!empty($part['internalPartNumber'])) { + $entity->setIpn($part['internalPartNumber']); + } + $entity->setComment($part['comment'] ?? ''); + $entity->setNeedsReview($part['needsReview'] === '1'); + $this->setCreationDate($entity, $part['createDate']); + + $this->setAssociationField($entity, 'footprint', Footprint::class, $part['footprint_id']); + $this->setAssociationField($entity, 'category', Category::class, $part['category_id']); + + //Set partUnit (when it is not ID=1, which is Pieces in Partkeepr) + if ($part['partUnit_id'] !== '1') { + $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); + } + + //Create a part lot to store the stock level and location + $lot = new PartLot(); + $lot->setAmount($part['stockLevel'] ?? 0); + $this->setAssociationField($lot, 'storage_location', Storelocation::class, $part['storageLocation_id']); + $entity->addPartLot($lot); + + //For partCondition, productionsRemarks and Status, create a custom parameter + if ($part['partCondition']) { + $partCondition = (new PartParameter())->setName('Part Condition')->setGroup('PartKeepr') + ->setValueText($part['partCondition']); + $entity->addParameter($partCondition); + } + if ($part['productionRemarks']) { + $partCondition = (new PartParameter())->setName('Production Remarks')->setGroup('PartKeepr') + ->setValueText($part['productionRemarks']); + $entity->addParameter($partCondition); + } + if ($part['status']) { + $partCondition = (new PartParameter())->setName('Status')->setGroup('PartKeepr') + ->setValueText($part['status']); + $entity->addParameter($partCondition); + } + } + + $this->setIDOfEntity($entity, $part['id']); + $this->em->persist($entity); + } + + $this->em->flush(); + + + return count($part_data); + } + /** * Assigns the parent to the given entity, using the numerical IDs from the imported data. * @param string $class @@ -263,7 +340,7 @@ class PartkeeprImporter * @param int|string $parent_id * @return AbstractStructuralDBElement The structural element that was modified (with $element_id) */ - private function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement + protected function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement { $element = $this->em->find($class, (int) $element_id); if (!$element) { @@ -284,13 +361,34 @@ class PartkeeprImporter return $element; } + /** + * Sets the given field of the given entity to the entity with the given ID. + * @return AbstractDBElement + */ + protected function setAssociationField(AbstractDBElement $element, string $field, string $other_class, $other_id): AbstractDBElement + { + //If the parent is null, set the field to null and we're done + if (!$other_id) { + $this->propertyAccessor->setValue($element, $field, null); + return $element; + } + + $parent = $this->em->find($other_class, (int) $other_id); + if (!$parent) { + throw new \RuntimeException(sprintf('Could not find other_class with ID %s', $other_id)); + } + + $this->propertyAccessor->setValue($element, $field, $parent); + return $element; + } + /** * Set the ID of an entity to a specific value. Must be called before persisting the entity, but before flushing. * @param AbstractDBElement $element * @param int|string $id * @return void */ - public function setIDOfEntity(AbstractDBElement $element, $id): void + protected function setIDOfEntity(AbstractDBElement $element, $id): void { if (!is_int($id) && !is_string($id)) { throw new \InvalidArgumentException('ID must be an integer or string'); @@ -303,4 +401,23 @@ class PartkeeprImporter $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); $metadata->setIdentifierValues($element, ['id' => $id]); } + + /** + * Sets the creation date of an entity to a specific value. + * @return void + * @throws \Exception + */ + protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str) + { + if ($datetime_str) { + $date = new \DateTime($datetime_str); + } else { + $date = null; //Null means "now" at persist time + } + + $reflectionClass = new \ReflectionClass($entity); + $property = $reflectionClass->getProperty('addedDate'); + $property->setAccessible(true); + $property->setValue($entity, $date); + } } \ No newline at end of file From c972f0ac5967aefb146ca1926472c153c03a2947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 00:12:36 +0100 Subject: [PATCH 05/12] Added possibility to import Part manufacturer and parameter information --- .../ImportExportSystem/PartkeeprImporter.php | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php index bb23b095..82880dc8 100644 --- a/src/Services/ImportExportSystem/PartkeeprImporter.php +++ b/src/Services/ImportExportSystem/PartkeeprImporter.php @@ -276,6 +276,7 @@ class PartkeeprImporter $entity->setDescription($part['description'] ?? ''); //All parts get a tag, that they were imported from PartKeepr $entity->setTags('partkeepr-imported'); + $this->setAssociationField($entity, 'category', Category::class, $part['category_id']); //If the part is a metapart, write that in the description, and we can skip the rest if ($part['metaPart'] === '1') { @@ -292,7 +293,6 @@ class PartkeeprImporter $this->setCreationDate($entity, $part['createDate']); $this->setAssociationField($entity, 'footprint', Footprint::class, $part['footprint_id']); - $this->setAssociationField($entity, 'category', Category::class, $part['category_id']); //Set partUnit (when it is not ID=1, which is Pieces in Partkeepr) if ($part['partUnit_id'] !== '1') { @@ -329,10 +329,90 @@ class PartkeeprImporter $this->em->flush(); + $this->importPartManufacturers($data); + $this->importPartParameters($data); return count($part_data); } + protected function importPartManufacturers(array $data): void + { + if (!isset($data['partmanufacturer'])) { + throw new \RuntimeException('$data must contain a "partmanufacturer" key!'); + } + + //Part-DB only supports one manufacturer per part, only the last one is imported + $partmanufacturer_data = $data['partmanufacturer']; + foreach ($partmanufacturer_data as $partmanufacturer) { + /** @var Part $part */ + $part = $this->em->find(Part::class, (int) $partmanufacturer['part_id']); + if (!$part) { + throw new \RuntimeException(sprintf('Could not find part with ID %s', $partmanufacturer['part_id'])); + } + $manufacturer = $this->em->find(Manufacturer::class, (int) $partmanufacturer['manufacturer_id']); + if (!$manufacturer) { + throw new \RuntimeException(sprintf('Could not find manufacturer with ID %s', $partmanufacturer['manufacturer_id'])); + } + $part->setManufacturer($manufacturer); + $part->setManufacturerProductNumber($partmanufacturer['partNumber']); + } + + $this->em->flush(); + } + + protected function importPartParameters(array $data): void + { + if (!isset($data['partparameter'])) { + throw new \RuntimeException('$data must contain a "partparameter" key!'); + } + + foreach ($data['partparameter'] as $partparameter) { + $entity = new PartParameter(); + + //Name format: Name (Description) + $name = $partparameter['name']; + if (!empty($partparameter['description'])) { + $name .= ' ('.$partparameter['description'].')'; + } + $entity->setName($name); + + $entity->setValueText($partparameter['stringValue'] ?? ''); + $entity->setUnit($this->getUnitSymbol($data, (int) $partparameter['unit_id'])); + + $entity->setValueMin($partparameter['normalizedMinValue'] ?? null); + $entity->setValueTypical($partparameter['normalizedValue'] ?? null); + $entity->setValueMax($partparameter['normalizedMaxValue'] ?? null); + + $part = $this->em->find(Part::class, (int) $partparameter['part_id']); + if (!$part) { + throw new \RuntimeException(sprintf('Could not find part with ID %s', $partparameter['part_id'])); + } + + $part->addParameter($entity); + $this->em->persist($entity); + } + $this->em->flush(); + } + + + + /** + * Returns the (parameter) unit symbol for the given ID. + * @param array $data + * @param int $id + * @return string + */ + protected function getUnitSymbol(array $data, int $id): string + { + foreach ($data['unit'] as $unit) { + if ((int) $unit['id'] === $id) { + return $unit['symbol']; + } + } + + throw new \RuntimeException(sprintf('Could not find unit with ID %s', $id)); + } + /** * Assigns the parent to the given entity, using the numerical IDs from the imported data. * @param string $class From 46beb21ba7562db09c4216b489a91443c8b6b0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 00:25:18 +0100 Subject: [PATCH 06/12] Improved structure of the PartKeepr import --- .../Migrations/ImportPartKeeprCommand.php | 35 +- .../MySQLDumpXMLConverter.php | 2 +- .../PKDatastructureImporter.php | 259 +++++++++ .../PartKeeprImporter/PKImportHelper.php | 50 ++ .../PartKeeprImporter/PKImportHelperTrait.php | 125 +++++ .../PartKeeprImporter/PKPartImporter.php | 195 +++++++ .../ImportExportSystem/PartkeeprImporter.php | 503 ------------------ .../Misc/MySQLDumpXMLConverterTest.php | 2 +- 8 files changed, 652 insertions(+), 519 deletions(-) rename src/Services/{Misc => ImportExportSystem/PartKeeprImporter}/MySQLDumpXMLConverter.php (98%) create mode 100644 src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php create mode 100644 src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php create mode 100644 src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php create mode 100644 src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php delete mode 100644 src/Services/ImportExportSystem/PartkeeprImporter.php diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index 87069389..53aecc04 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -20,8 +20,10 @@ namespace App\Command\Migrations; -use App\Services\ImportExportSystem\PartkeeprImporter; -use App\Services\Misc\MySQLDumpXMLConverter; +use App\Services\ImportExportSystem\PartKeeprImporter\PKDatastructureImporter; +use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter; +use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper; +use App\Services\ImportExportSystem\PartKeeprImporter\PKPartImporter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -35,14 +37,19 @@ class ImportPartKeeprCommand extends Command protected static $defaultName = 'partdb:import-partkeepr'; protected EntityManagerInterface $em; - protected PartkeeprImporter $importer; protected MySQLDumpXMLConverter $xml_converter; + protected PKDatastructureImporter $datastructureImporter; + protected PKImportHelper $importHelper; + protected PKPartImporter $partImporter; - public function __construct(EntityManagerInterface $em, PartkeeprImporter $importer, MySQLDumpXMLConverter $xml_converter) + public function __construct(EntityManagerInterface $em, MySQLDumpXMLConverter $xml_converter, + PKDatastructureImporter $datastructureImporter, PKPartImporter $partImporter, PKImportHelper $importHelper) { parent::__construct(self::$defaultName); $this->em = $em; - $this->importer = $importer; + $this->datastructureImporter = $datastructureImporter; + $this->importHelper = $importHelper; + $this->partImporter = $partImporter; $this->xml_converter = $xml_converter; } @@ -63,7 +70,7 @@ class ImportPartKeeprCommand extends Command //$io->confirm('This will delete all data in the database. Do you want to continue?', false); //Purge the databse, so we will not have any conflicts - $this->importer->purgeDatabaseForImport(); + $this->importHelper->purgeDatabaseForImport(); //Convert the XML file to an array $xml = file_get_contents($input_path); @@ -75,37 +82,37 @@ class ImportPartKeeprCommand extends Command return 0; } - private function doImport(SymfonyStyle $io, array $data) + private function doImport(SymfonyStyle $io, array $data): void { //First import the distributors $io->info('Importing distributors...'); - $count = $this->importer->importDistributors($data); + $count = $this->datastructureImporter->importDistributors($data); $io->success('Imported '.$count.' distributors.'); //Import the measurement units $io->info('Importing part measurement units...'); - $count = $this->importer->importPartUnits($data); + $count = $this->datastructureImporter->importPartUnits($data); $io->success('Imported '.$count.' measurement units.'); //Import manufacturers $io->info('Importing manufacturers...'); - $count = $this->importer->importManufacturers($data); + $count = $this->datastructureImporter->importManufacturers($data); $io->success('Imported '.$count.' manufacturers.'); $io->info('Importing categories...'); - $count = $this->importer->importCategories($data); + $count = $this->datastructureImporter->importCategories($data); $io->success('Imported '.$count.' categories.'); $io->info('Importing Footprints...'); - $count = $this->importer->importFootprints($data); + $count = $this->datastructureImporter->importFootprints($data); $io->success('Imported '.$count.' footprints.'); $io->info('Importing storage locations...'); - $count = $this->importer->importStorelocations($data); + $count = $this->datastructureImporter->importStorelocations($data); $io->success('Imported '.$count.' storage locations.'); $io->info('Importing parts...'); - $count = $this->importer->importParts($data); + $count = $this->partImporter->importParts($data); $io->success('Imported '.$count.' parts.'); } diff --git a/src/Services/Misc/MySQLDumpXMLConverter.php b/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php similarity index 98% rename from src/Services/Misc/MySQLDumpXMLConverter.php rename to src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php index 9f9a01fc..9d583d2a 100644 --- a/src/Services/Misc/MySQLDumpXMLConverter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace App\Services\Misc; +namespace App\Services\ImportExportSystem\PartKeeprImporter; class MySQLDumpXMLConverter { diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php new file mode 100644 index 00000000..81df1c44 --- /dev/null +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -0,0 +1,259 @@ +. + */ + +namespace App\Services\ImportExportSystem\PartKeeprImporter; + +use App\Doctrine\Purger\ResetAutoIncrementORMPurger; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Contracts\TimeStampableInterface; +use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\Storelocation; +use App\Entity\Parts\Supplier; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +use function \count; + +/** + * This service is used to import the datastructures (categories, manufacturers, etc.) from a PartKeepr export. + */ +class PKDatastructureImporter +{ + + use PKImportHelperTrait; + + public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) + { + $this->em = $em; + $this->propertyAccessor = $propertyAccessor; + } + + + /** + * Imports the distributors from the given data. + * @param array $data The data to import (associated array, containing a 'distributor' key + * @return int The number of imported distributors + */ + public function importDistributors(array $data): int + { + if (!isset($data['distributor'])) { + throw new \RuntimeException('$data must contain a "distributor" key!'); + } + + $distributor_data = $data['distributor']; + + foreach ($distributor_data as $distributor) { + $supplier = new Supplier(); + $supplier->setName($distributor['name']); + $supplier->setWebsite($distributor['url'] ?? ''); + $supplier->setAddress($distributor['address'] ?? ''); + $supplier->setPhoneNumber($distributor['phone'] ?? ''); + $supplier->setFaxNumber($distributor['fax'] ?? ''); + $supplier->setEmailAddress($distributor['email'] ?? ''); + $supplier->setComment($distributor['comment']); + $supplier->setAutoProductUrl($distributor['skuurl'] ?? ''); + + $this->setIDOfEntity($supplier, $distributor['id']); + $this->em->persist($supplier); + } + + $this->em->flush(); + + return count($distributor_data); + } + + public function importManufacturers(array $data): int + { + if (!isset($data['manufacturer'])) { + throw new \RuntimeException('$data must contain a "manufacturer" key!'); + } + + $manufacturer_data = $data['manufacturer']; + + $max_id = 0; + + //Assign a parent manufacturer to all manufacturers, as partkeepr has a lot of manufacturers by default + $parent_manufacturer = new Manufacturer(); + $parent_manufacturer->setName('PartKeepr'); + $parent_manufacturer->setNotSelectable(true); + + foreach ($manufacturer_data as $manufacturer) { + $entity = new Manufacturer(); + $entity->setName($manufacturer['name']); + $entity->setWebsite($manufacturer['url'] ?? ''); + $entity->setAddress($manufacturer['address'] ?? ''); + $entity->setPhoneNumber($manufacturer['phone'] ?? ''); + $entity->setFaxNumber($manufacturer['fax'] ?? ''); + $entity->setEmailAddress($manufacturer['email'] ?? ''); + $entity->setComment($manufacturer['comment']); + $entity->setParent($parent_manufacturer); + + $this->setIDOfEntity($entity, $manufacturer['id']); + $this->em->persist($entity); + + $max_id = max($max_id, $manufacturer['id']); + } + + //Set the ID of the parent manufacturer to the max ID + 1, to avoid trouble with the auto increment + $this->setIDOfEntity($parent_manufacturer, $max_id + 1); + $this->em->persist($parent_manufacturer); + + $this->em->flush(); + + return count($manufacturer_data); + } + + public function importPartUnits(array $data): int + { + if (!isset($data['partunit'])) { + throw new \RuntimeException('$data must contain a "partunit" key!'); + } + + $partunit_data = $data['partunit']; + foreach ($partunit_data as $partunit) { + $unit = new MeasurementUnit(); + $unit->setName($partunit['name']); + $unit->setUnit($partunit['shortName'] ?? null); + + $this->setIDOfEntity($unit, $partunit['id']); + $this->em->persist($unit); + } + + $this->em->flush(); + + return count($partunit_data); + } + + public function importCategories(array $data): int + { + if (!isset($data['partcategory'])) { + throw new \RuntimeException('$data must contain a "partcategory" key!'); + } + + $partcategory_data = $data['partcategory']; + + //In a first step, create all categories like they were a flat structure (so ignore the parent) + foreach ($partcategory_data as $partcategory) { + $category = new Category(); + $category->setName($partcategory['name']); + $category->setComment($partcategory['description']); + + $this->setIDOfEntity($category, $partcategory['id']); + $this->em->persist($category); + } + + $this->em->flush(); + + //In a second step, set the correct parent element + foreach ($partcategory_data as $partcategory) { + $this->setParent(Category::class, $partcategory['id'], $partcategory['parent_id']); + } + $this->em->flush(); + + return count($partcategory_data); + } + + /** + * The common import functions for footprints and storeloactions + * @param array $data + * @param string $target_class + * @param string $data_prefix + * @return int + */ + private function importElementsWithCategory(array $data, string $target_class, string $data_prefix): int + { + $key = $data_prefix; + $category_key = $data_prefix.'category'; + + if (!isset($data[$key])) { + throw new \RuntimeException('$data must contain a "'. $key .'" key!'); + } + if (!isset($data[$category_key])) { + throw new \RuntimeException('$data must contain a "'. $category_key .'" key!'); + } + + //We import the footprints first, as we need the IDs of the footprints be our real DBs later (as we match the part import by ID) + //As the footprints category is not existing yet, we just skip the parent field for now + $footprint_data = $data[$key]; + $max_footprint_id = 0; + foreach ($footprint_data as $footprint) { + $entity = new $target_class(); + $entity->setName($footprint['name']); + $entity->setComment($footprint['description'] ?? ''); + + $this->setIDOfEntity($entity, $footprint['id']); + $this->em->persist($entity); + $max_footprint_id = max($max_footprint_id, (int) $footprint['id']); + } + + //Import the footprint categories ignoring the parents for now + //Their IDs are $max_footprint_id + $ID + $footprintcategory_data = $data[$category_key]; + foreach ($footprintcategory_data as $footprintcategory) { + $entity = new $target_class(); + $entity->setName($footprintcategory['name']); + $entity->setComment($footprintcategory['description']); + //Categories are not assignable to parts, so we set them to not selectable + $entity->setNotSelectable(true); + + $this->setIDOfEntity($entity, $max_footprint_id + (int) $footprintcategory['id']); + $this->em->persist($entity); + } + + $this->em->flush(); + + //Now we can correct the parents and category IDs of the parts + foreach ($footprintcategory_data as $footprintcategory) { + //We have to use the mapped IDs here, as the imported ID is not the effective ID + if ($footprintcategory['parent_id']) { + $this->setParent($target_class, $max_footprint_id + (int)$footprintcategory['id'], + $max_footprint_id + (int)$footprintcategory['parent_id']); + } + } + foreach ($footprint_data as $footprint) { + if ($footprint['category_id']) { + $this->setParent($target_class, $footprint['id'], + $max_footprint_id + (int)$footprint['category_id']); + } + } + + $this->em->flush(); + + return count($footprint_data) + count($footprintcategory_data); + } + + public function importFootprints(array $data): int + { + return $this->importElementsWithCategory($data, Footprint::class, 'footprint'); + } + + public function importStorelocations(array $data): int + { + return $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); + } +} \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php new file mode 100644 index 00000000..632bcd39 --- /dev/null +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php @@ -0,0 +1,50 @@ +. + */ + +namespace App\Services\ImportExportSystem\PartKeeprImporter; + +use App\Doctrine\Purger\ResetAutoIncrementORMPurger; +use Doctrine\ORM\EntityManagerInterface; + +/** + * This service contains various helper functions for the PartKeeprImporter (like purging the database). + */ +class PKImportHelper +{ + protected EntityManagerInterface $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + /** + * Purges the database tables for the import, so that all data can be created from scratch. + * Existing users and groups are not purged. + * This is needed to avoid ID collisions. + * @return void + */ + public function purgeDatabaseForImport(): void + { + //Versions with "" are needed !! + $purger = new ResetAutoIncrementORMPurger($this->em, ['users', '"users"', 'groups', '"groups"', 'u2f_keys', 'internal', 'migration_versions']); + $purger->purge(); + } +} \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php new file mode 100644 index 00000000..2d9b456d --- /dev/null +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -0,0 +1,125 @@ +. + */ + +namespace App\Services\ImportExportSystem\PartKeeprImporter; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Contracts\TimeStampableInterface; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * This trait contains helper functions for the PartKeeprImporter. + */ +trait PKImportHelperTrait +{ + protected EntityManagerInterface $em; + protected PropertyAccessorInterface $propertyAccessor; + + /** + * Assigns the parent to the given entity, using the numerical IDs from the imported data. + * @param string $class + * @param int|string $element_id + * @param int|string $parent_id + * @return AbstractStructuralDBElement The structural element that was modified (with $element_id) + */ + protected function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement + { + $element = $this->em->find($class, (int) $element_id); + if (!$element) { + throw new \RuntimeException(sprintf('Could not find element with ID %s', $element_id)); + } + + //If the parent is null, we're done + if (!$parent_id) { + return $element; + } + + $parent = $this->em->find($class, (int) $parent_id); + if (!$parent) { + throw new \RuntimeException(sprintf('Could not find parent with ID %s', $parent_id)); + } + + $element->setParent($parent); + return $element; + } + + /** + * Sets the given field of the given entity to the entity with the given ID. + * @return AbstractDBElement + */ + protected function setAssociationField(AbstractDBElement $element, string $field, string $other_class, $other_id): AbstractDBElement + { + //If the parent is null, set the field to null and we're done + if (!$other_id) { + $this->propertyAccessor->setValue($element, $field, null); + return $element; + } + + $parent = $this->em->find($other_class, (int) $other_id); + if (!$parent) { + throw new \RuntimeException(sprintf('Could not find other_class with ID %s', $other_id)); + } + + $this->propertyAccessor->setValue($element, $field, $parent); + return $element; + } + + /** + * Set the ID of an entity to a specific value. Must be called before persisting the entity, but before flushing. + * @param AbstractDBElement $element + * @param int|string $id + * @return void + */ + protected function setIDOfEntity(AbstractDBElement $element, $id): void + { + if (!is_int($id) && !is_string($id)) { + throw new \InvalidArgumentException('ID must be an integer or string'); + } + + $id = (int) $id; + + $metadata = $this->em->getClassMetadata(get_class($element)); + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); + $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); + $metadata->setIdentifierValues($element, ['id' => $id]); + } + + /** + * Sets the creation date of an entity to a specific value. + * @return void + * @throws \Exception + */ + protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str) + { + if ($datetime_str) { + $date = new \DateTime($datetime_str); + } else { + $date = null; //Null means "now" at persist time + } + + $reflectionClass = new \ReflectionClass($entity); + $property = $reflectionClass->getProperty('addedDate'); + $property->setAccessible(true); + $property->setValue($entity, $date); + } +} \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php new file mode 100644 index 00000000..84b2b9fe --- /dev/null +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -0,0 +1,195 @@ +. + */ + +namespace App\Services\ImportExportSystem\PartKeeprImporter; + +use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\Storelocation; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * This service is used to import parts from a PartKeepr export. You have to import the datastructures first! + */ +class PKPartImporter +{ + use PKImportHelperTrait; + + public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) + { + $this->em = $em; + $this->propertyAccessor = $propertyAccessor; + } + + public function importParts(array $data): int + { + if (!isset($data['part'])) { + throw new \RuntimeException('$data must contain a "part" key!'); + } + + + $part_data = $data['part']; + foreach ($part_data as $part) { + $entity = new Part(); + $entity->setName($part['name']); + $entity->setDescription($part['description'] ?? ''); + //All parts get a tag, that they were imported from PartKeepr + $entity->setTags('partkeepr-imported'); + $this->setAssociationField($entity, 'category', Category::class, $part['category_id']); + + //If the part is a metapart, write that in the description, and we can skip the rest + if ($part['metaPart'] === '1') { + $entity->setDescription('Metapart (Not supported in Part-DB)'); + $entity->setComment('This part represents a former metapart in PartKeepr. It is not supported in Part-DB yet. And you can most likely delete it.'); + $entity->setTags('partkeepr-imported,partkeepr-metapart'); + } else { + $entity->setMinAmount($part['minStockLevel'] ?? 0); + if (!empty($part['internalPartNumber'])) { + $entity->setIpn($part['internalPartNumber']); + } + $entity->setComment($part['comment'] ?? ''); + $entity->setNeedsReview($part['needsReview'] === '1'); + $this->setCreationDate($entity, $part['createDate']); + + $this->setAssociationField($entity, 'footprint', Footprint::class, $part['footprint_id']); + + //Set partUnit (when it is not ID=1, which is Pieces in Partkeepr) + if ($part['partUnit_id'] !== '1') { + $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); + } + + //Create a part lot to store the stock level and location + $lot = new PartLot(); + $lot->setAmount($part['stockLevel'] ?? 0); + $this->setAssociationField($lot, 'storage_location', Storelocation::class, $part['storageLocation_id']); + $entity->addPartLot($lot); + + //For partCondition, productionsRemarks and Status, create a custom parameter + if ($part['partCondition']) { + $partCondition = (new PartParameter())->setName('Part Condition')->setGroup('PartKeepr') + ->setValueText($part['partCondition']); + $entity->addParameter($partCondition); + } + if ($part['productionRemarks']) { + $partCondition = (new PartParameter())->setName('Production Remarks')->setGroup('PartKeepr') + ->setValueText($part['productionRemarks']); + $entity->addParameter($partCondition); + } + if ($part['status']) { + $partCondition = (new PartParameter())->setName('Status')->setGroup('PartKeepr') + ->setValueText($part['status']); + $entity->addParameter($partCondition); + } + } + + $this->setIDOfEntity($entity, $part['id']); + $this->em->persist($entity); + } + + $this->em->flush(); + + $this->importPartManufacturers($data); + $this->importPartParameters($data); + + return count($part_data); + } + + protected function importPartManufacturers(array $data): void + { + if (!isset($data['partmanufacturer'])) { + throw new \RuntimeException('$data must contain a "partmanufacturer" key!'); + } + + //Part-DB only supports one manufacturer per part, only the last one is imported + $partmanufacturer_data = $data['partmanufacturer']; + foreach ($partmanufacturer_data as $partmanufacturer) { + /** @var Part $part */ + $part = $this->em->find(Part::class, (int) $partmanufacturer['part_id']); + if (!$part) { + throw new \RuntimeException(sprintf('Could not find part with ID %s', $partmanufacturer['part_id'])); + } + $manufacturer = $this->em->find(Manufacturer::class, (int) $partmanufacturer['manufacturer_id']); + if (!$manufacturer) { + throw new \RuntimeException(sprintf('Could not find manufacturer with ID %s', $partmanufacturer['manufacturer_id'])); + } + $part->setManufacturer($manufacturer); + $part->setManufacturerProductNumber($partmanufacturer['partNumber']); + } + + $this->em->flush(); + } + + protected function importPartParameters(array $data): void + { + if (!isset($data['partparameter'])) { + throw new \RuntimeException('$data must contain a "partparameter" key!'); + } + + foreach ($data['partparameter'] as $partparameter) { + $entity = new PartParameter(); + + //Name format: Name (Description) + $name = $partparameter['name']; + if (!empty($partparameter['description'])) { + $name .= ' ('.$partparameter['description'].')'; + } + $entity->setName($name); + + $entity->setValueText($partparameter['stringValue'] ?? ''); + $entity->setUnit($this->getUnitSymbol($data, (int) $partparameter['unit_id'])); + + $entity->setValueMin($partparameter['normalizedMinValue'] ?? null); + $entity->setValueTypical($partparameter['normalizedValue'] ?? null); + $entity->setValueMax($partparameter['normalizedMaxValue'] ?? null); + + $part = $this->em->find(Part::class, (int) $partparameter['part_id']); + if (!$part) { + throw new \RuntimeException(sprintf('Could not find part with ID %s', $partparameter['part_id'])); + } + + $part->addParameter($entity); + $this->em->persist($entity); + } + $this->em->flush(); + } + + /** + * Returns the (parameter) unit symbol for the given ID. + * @param array $data + * @param int $id + * @return string + */ + protected function getUnitSymbol(array $data, int $id): string + { + foreach ($data['unit'] as $unit) { + if ((int) $unit['id'] === $id) { + return $unit['symbol']; + } + } + + throw new \RuntimeException(sprintf('Could not find unit with ID %s', $id)); + } +} \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartkeeprImporter.php b/src/Services/ImportExportSystem/PartkeeprImporter.php deleted file mode 100644 index 82880dc8..00000000 --- a/src/Services/ImportExportSystem/PartkeeprImporter.php +++ /dev/null @@ -1,503 +0,0 @@ -. - */ - -namespace App\Services\ImportExportSystem; - -use App\Doctrine\Purger\ResetAutoIncrementORMPurger; -use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory; -use App\Entity\Base\AbstractDBElement; -use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Contracts\TimeStampableInterface; -use App\Entity\Parameters\PartParameter; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; -use App\Entity\Parts\Supplier; -use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory; -use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -class PartkeeprImporter -{ - - protected EntityManagerInterface $em; - protected PropertyAccessorInterface $propertyAccessor; - - public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) - { - $this->em = $em; - $this->propertyAccessor = $propertyAccessor; - } - - public function purgeDatabaseForImport(): void - { - //Versions with "" are needed !! - $purger = new ResetAutoIncrementORMPurger($this->em, ['users', '"users"', 'groups', '"groups"', 'u2f_keys', 'internal', 'migration_versions']); - $purger->purge(); - } - - /** - * Imports the distributors from the given data. - * @param array $data The data to import (associated array, containing a 'distributor' key - * @return int The number of imported distributors - */ - public function importDistributors(array $data): int - { - if (!isset($data['distributor'])) { - throw new \RuntimeException('$data must contain a "distributor" key!'); - } - - $distributor_data = $data['distributor']; - - foreach ($distributor_data as $distributor) { - $supplier = new Supplier(); - $supplier->setName($distributor['name']); - $supplier->setWebsite($distributor['url'] ?? ''); - $supplier->setAddress($distributor['address'] ?? ''); - $supplier->setPhoneNumber($distributor['phone'] ?? ''); - $supplier->setFaxNumber($distributor['fax'] ?? ''); - $supplier->setEmailAddress($distributor['email'] ?? ''); - $supplier->setComment($distributor['comment']); - $supplier->setAutoProductUrl($distributor['skuurl'] ?? ''); - - $this->setIDOfEntity($supplier, $distributor['id']); - $this->em->persist($supplier); - } - - $this->em->flush(); - - return count($distributor_data); - } - - public function importManufacturers(array $data): int - { - if (!isset($data['manufacturer'])) { - throw new \RuntimeException('$data must contain a "manufacturer" key!'); - } - - $manufacturer_data = $data['manufacturer']; - - $max_id = 0; - - //Assign a parent manufacturer to all manufacturers, as partkeepr has a lot of manufacturers by default - $parent_manufacturer = new Manufacturer(); - $parent_manufacturer->setName('PartKeepr'); - $parent_manufacturer->setNotSelectable(true); - - foreach ($manufacturer_data as $manufacturer) { - $entity = new Manufacturer(); - $entity->setName($manufacturer['name']); - $entity->setWebsite($manufacturer['url'] ?? ''); - $entity->setAddress($manufacturer['address'] ?? ''); - $entity->setPhoneNumber($manufacturer['phone'] ?? ''); - $entity->setFaxNumber($manufacturer['fax'] ?? ''); - $entity->setEmailAddress($manufacturer['email'] ?? ''); - $entity->setComment($manufacturer['comment']); - $entity->setParent($parent_manufacturer); - - $this->setIDOfEntity($entity, $manufacturer['id']); - $this->em->persist($entity); - - $max_id = max($max_id, $manufacturer['id']); - } - - //Set the ID of the parent manufacturer to the max ID + 1, to avoid trouble with the auto increment - $this->setIDOfEntity($parent_manufacturer, $max_id + 1); - $this->em->persist($parent_manufacturer); - - $this->em->flush(); - - return count($manufacturer_data); - } - - public function importPartUnits(array $data): int - { - if (!isset($data['partunit'])) { - throw new \RuntimeException('$data must contain a "partunit" key!'); - } - - $partunit_data = $data['partunit']; - foreach ($partunit_data as $partunit) { - $unit = new MeasurementUnit(); - $unit->setName($partunit['name']); - $unit->setUnit($partunit['shortName'] ?? null); - - $this->setIDOfEntity($unit, $partunit['id']); - $this->em->persist($unit); - } - - $this->em->flush(); - - return count($partunit_data); - } - - public function importCategories(array $data): int - { - if (!isset($data['partcategory'])) { - throw new \RuntimeException('$data must contain a "partcategory" key!'); - } - - $partcategory_data = $data['partcategory']; - - //In a first step, create all categories like they were a flat structure (so ignore the parent) - foreach ($partcategory_data as $partcategory) { - $category = new Category(); - $category->setName($partcategory['name']); - $category->setComment($partcategory['description']); - - $this->setIDOfEntity($category, $partcategory['id']); - $this->em->persist($category); - } - - $this->em->flush(); - - //In a second step, set the correct parent element - foreach ($partcategory_data as $partcategory) { - $this->setParent(Category::class, $partcategory['id'], $partcategory['parent_id']); - } - $this->em->flush(); - - return count($partcategory_data); - } - - /** - * The common import functions for footprints and storeloactions - * @param array $data - * @param string $target_class - * @param string $data_prefix - * @return int - */ - private function importElementsWithCategory(array $data, string $target_class, string $data_prefix): int - { - $key = $data_prefix; - $category_key = $data_prefix.'category'; - - if (!isset($data[$key])) { - throw new \RuntimeException('$data must contain a "'. $key .'" key!'); - } - if (!isset($data[$category_key])) { - throw new \RuntimeException('$data must contain a "'. $category_key .'" key!'); - } - - //We import the footprints first, as we need the IDs of the footprints be our real DBs later (as we match the part import by ID) - //As the footprints category is not existing yet, we just skip the parent field for now - $footprint_data = $data[$key]; - $max_footprint_id = 0; - foreach ($footprint_data as $footprint) { - $entity = new $target_class(); - $entity->setName($footprint['name']); - $entity->setComment($footprint['description'] ?? ''); - - $this->setIDOfEntity($entity, $footprint['id']); - $this->em->persist($entity); - $max_footprint_id = max($max_footprint_id, (int) $footprint['id']); - } - - //Import the footprint categories ignoring the parents for now - //Their IDs are $max_footprint_id + $ID - $footprintcategory_data = $data[$category_key]; - foreach ($footprintcategory_data as $footprintcategory) { - $entity = new $target_class(); - $entity->setName($footprintcategory['name']); - $entity->setComment($footprintcategory['description']); - //Categories are not assignable to parts, so we set them to not selectable - $entity->setNotSelectable(true); - - $this->setIDOfEntity($entity, $max_footprint_id + (int) $footprintcategory['id']); - $this->em->persist($entity); - } - - $this->em->flush(); - - //Now we can correct the parents and category IDs of the parts - foreach ($footprintcategory_data as $footprintcategory) { - //We have to use the mapped IDs here, as the imported ID is not the effective ID - if ($footprintcategory['parent_id']) { - $this->setParent($target_class, $max_footprint_id + (int)$footprintcategory['id'], - $max_footprint_id + (int)$footprintcategory['parent_id']); - } - } - foreach ($footprint_data as $footprint) { - if ($footprint['category_id']) { - $this->setParent($target_class, $footprint['id'], - $max_footprint_id + (int)$footprint['category_id']); - } - } - - $this->em->flush(); - - return count($footprint_data) + count($footprintcategory_data); - } - - public function importFootprints(array $data): int - { - return $this->importElementsWithCategory($data, Footprint::class, 'footprint'); - } - - public function importStorelocations(array $data): int - { - return $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); - } - - public function importParts(array $data): int - { - if (!isset($data['part'])) { - throw new \RuntimeException('$data must contain a "part" key!'); - } - - - $part_data = $data['part']; - foreach ($part_data as $part) { - $entity = new Part(); - $entity->setName($part['name']); - $entity->setDescription($part['description'] ?? ''); - //All parts get a tag, that they were imported from PartKeepr - $entity->setTags('partkeepr-imported'); - $this->setAssociationField($entity, 'category', Category::class, $part['category_id']); - - //If the part is a metapart, write that in the description, and we can skip the rest - if ($part['metaPart'] === '1') { - $entity->setDescription('Metapart (Not supported in Part-DB)'); - $entity->setComment('This part represents a former metapart in PartKeepr. It is not supported in Part-DB yet. And you can most likely delete it.'); - $entity->setTags('partkeepr-imported,partkeepr-metapart'); - } else { - $entity->setMinAmount($part['minStockLevel'] ?? 0); - if (!empty($part['internalPartNumber'])) { - $entity->setIpn($part['internalPartNumber']); - } - $entity->setComment($part['comment'] ?? ''); - $entity->setNeedsReview($part['needsReview'] === '1'); - $this->setCreationDate($entity, $part['createDate']); - - $this->setAssociationField($entity, 'footprint', Footprint::class, $part['footprint_id']); - - //Set partUnit (when it is not ID=1, which is Pieces in Partkeepr) - if ($part['partUnit_id'] !== '1') { - $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); - } - - //Create a part lot to store the stock level and location - $lot = new PartLot(); - $lot->setAmount($part['stockLevel'] ?? 0); - $this->setAssociationField($lot, 'storage_location', Storelocation::class, $part['storageLocation_id']); - $entity->addPartLot($lot); - - //For partCondition, productionsRemarks and Status, create a custom parameter - if ($part['partCondition']) { - $partCondition = (new PartParameter())->setName('Part Condition')->setGroup('PartKeepr') - ->setValueText($part['partCondition']); - $entity->addParameter($partCondition); - } - if ($part['productionRemarks']) { - $partCondition = (new PartParameter())->setName('Production Remarks')->setGroup('PartKeepr') - ->setValueText($part['productionRemarks']); - $entity->addParameter($partCondition); - } - if ($part['status']) { - $partCondition = (new PartParameter())->setName('Status')->setGroup('PartKeepr') - ->setValueText($part['status']); - $entity->addParameter($partCondition); - } - } - - $this->setIDOfEntity($entity, $part['id']); - $this->em->persist($entity); - } - - $this->em->flush(); - - $this->importPartManufacturers($data); - $this->importPartParameters($data); - - return count($part_data); - } - - protected function importPartManufacturers(array $data): void - { - if (!isset($data['partmanufacturer'])) { - throw new \RuntimeException('$data must contain a "partmanufacturer" key!'); - } - - //Part-DB only supports one manufacturer per part, only the last one is imported - $partmanufacturer_data = $data['partmanufacturer']; - foreach ($partmanufacturer_data as $partmanufacturer) { - /** @var Part $part */ - $part = $this->em->find(Part::class, (int) $partmanufacturer['part_id']); - if (!$part) { - throw new \RuntimeException(sprintf('Could not find part with ID %s', $partmanufacturer['part_id'])); - } - $manufacturer = $this->em->find(Manufacturer::class, (int) $partmanufacturer['manufacturer_id']); - if (!$manufacturer) { - throw new \RuntimeException(sprintf('Could not find manufacturer with ID %s', $partmanufacturer['manufacturer_id'])); - } - $part->setManufacturer($manufacturer); - $part->setManufacturerProductNumber($partmanufacturer['partNumber']); - } - - $this->em->flush(); - } - - protected function importPartParameters(array $data): void - { - if (!isset($data['partparameter'])) { - throw new \RuntimeException('$data must contain a "partparameter" key!'); - } - - foreach ($data['partparameter'] as $partparameter) { - $entity = new PartParameter(); - - //Name format: Name (Description) - $name = $partparameter['name']; - if (!empty($partparameter['description'])) { - $name .= ' ('.$partparameter['description'].')'; - } - $entity->setName($name); - - $entity->setValueText($partparameter['stringValue'] ?? ''); - $entity->setUnit($this->getUnitSymbol($data, (int) $partparameter['unit_id'])); - - $entity->setValueMin($partparameter['normalizedMinValue'] ?? null); - $entity->setValueTypical($partparameter['normalizedValue'] ?? null); - $entity->setValueMax($partparameter['normalizedMaxValue'] ?? null); - - $part = $this->em->find(Part::class, (int) $partparameter['part_id']); - if (!$part) { - throw new \RuntimeException(sprintf('Could not find part with ID %s', $partparameter['part_id'])); - } - - $part->addParameter($entity); - $this->em->persist($entity); - } - $this->em->flush(); - } - - - - /** - * Returns the (parameter) unit symbol for the given ID. - * @param array $data - * @param int $id - * @return string - */ - protected function getUnitSymbol(array $data, int $id): string - { - foreach ($data['unit'] as $unit) { - if ((int) $unit['id'] === $id) { - return $unit['symbol']; - } - } - - throw new \RuntimeException(sprintf('Could not find unit with ID %s', $id)); - } - - /** - * Assigns the parent to the given entity, using the numerical IDs from the imported data. - * @param string $class - * @param int|string $element_id - * @param int|string $parent_id - * @return AbstractStructuralDBElement The structural element that was modified (with $element_id) - */ - protected function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement - { - $element = $this->em->find($class, (int) $element_id); - if (!$element) { - throw new \RuntimeException(sprintf('Could not find element with ID %s', $element_id)); - } - - //If the parent is null, we're done - if (!$parent_id) { - return $element; - } - - $parent = $this->em->find($class, (int) $parent_id); - if (!$parent) { - throw new \RuntimeException(sprintf('Could not find parent with ID %s', $parent_id)); - } - - $element->setParent($parent); - return $element; - } - - /** - * Sets the given field of the given entity to the entity with the given ID. - * @return AbstractDBElement - */ - protected function setAssociationField(AbstractDBElement $element, string $field, string $other_class, $other_id): AbstractDBElement - { - //If the parent is null, set the field to null and we're done - if (!$other_id) { - $this->propertyAccessor->setValue($element, $field, null); - return $element; - } - - $parent = $this->em->find($other_class, (int) $other_id); - if (!$parent) { - throw new \RuntimeException(sprintf('Could not find other_class with ID %s', $other_id)); - } - - $this->propertyAccessor->setValue($element, $field, $parent); - return $element; - } - - /** - * Set the ID of an entity to a specific value. Must be called before persisting the entity, but before flushing. - * @param AbstractDBElement $element - * @param int|string $id - * @return void - */ - protected function setIDOfEntity(AbstractDBElement $element, $id): void - { - if (!is_int($id) && !is_string($id)) { - throw new \InvalidArgumentException('ID must be an integer or string'); - } - - $id = (int) $id; - - $metadata = $this->em->getClassMetadata(get_class($element)); - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); - $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); - $metadata->setIdentifierValues($element, ['id' => $id]); - } - - /** - * Sets the creation date of an entity to a specific value. - * @return void - * @throws \Exception - */ - protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str) - { - if ($datetime_str) { - $date = new \DateTime($datetime_str); - } else { - $date = null; //Null means "now" at persist time - } - - $reflectionClass = new \ReflectionClass($entity); - $property = $reflectionClass->getProperty('addedDate'); - $property->setAccessible(true); - $property->setValue($entity, $date); - } -} \ No newline at end of file diff --git a/tests/Services/Misc/MySQLDumpXMLConverterTest.php b/tests/Services/Misc/MySQLDumpXMLConverterTest.php index 40ef2718..9f2a9925 100644 --- a/tests/Services/Misc/MySQLDumpXMLConverterTest.php +++ b/tests/Services/Misc/MySQLDumpXMLConverterTest.php @@ -20,7 +20,7 @@ namespace App\Tests\Services\Misc; -use App\Services\Misc\MySQLDumpXMLConverter; +use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter; use PHPUnit\Framework\TestCase; class MySQLDumpXMLConverterTest extends TestCase From 7220d752ac66c401b10d635422ed3648a43df6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 16:26:39 +0100 Subject: [PATCH 07/12] Added possibilities to import part distributor infos --- .../PartKeeprImporter/PKPartImporter.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index 84b2b9fe..a708e997 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -28,6 +28,10 @@ use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use Brick\Math\BigDecimal; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -113,6 +117,7 @@ class PKPartImporter $this->importPartManufacturers($data); $this->importPartParameters($data); + $this->importOrderdetails($data); return count($part_data); } @@ -176,6 +181,64 @@ class PKPartImporter $this->em->flush(); } + protected function importOrderdetails(array $data): void + { + if (!isset($data['partdistributor'])) { + throw new \RuntimeException('$data must contain a "partdistributor" key!'); + } + + foreach ($data['partdistributor'] as $partdistributor) { + //Retrieve the part + $part = $this->em->find(Part::class, (int) $partdistributor['part_id']); + if (!$part) { + throw new \RuntimeException(sprintf('Could not find part with ID %s', $partdistributor['part_id'])); + } + //Retrieve the distributor + $supplier = $this->em->find(Supplier::class, (int) $partdistributor['distributor_id']); + if (!$supplier) { + throw new \RuntimeException(sprintf('Could not find supplier with ID %s', $partdistributor['distributor_id'])); + } + + //Check if the part already has an orderdetail for this supplier and ordernumber + if (empty($partdistributor['orderNumber']) && !empty($partdistributor['sku'])) { + $spn = $partdistributor['sku']; + } elseif (!empty($partdistributor['orderNumber']) && empty($partdistributor['sku'])) { + $spn = $partdistributor['orderNumber']; + } elseif (!empty($partdistributor['orderNumber']) && !empty($partdistributor['sku'])) { + $spn = $partdistributor['orderNumber'] . ' (' . $partdistributor['sku'] . ')'; + } else { + $spn = 'PartKeepr Import'; + } + + $orderdetail = $this->em->getRepository(Orderdetail::class)->findOneBy([ + 'part' => $part, + 'supplier' => $supplier, + 'supplierpartnr' => $spn, + ]); + + //When no orderdetail exists, create one + if (!$orderdetail) { + $orderdetail = new Orderdetail(); + $orderdetail->setSupplier($supplier); + $orderdetail->setSupplierpartnr($spn); + $part->addOrderdetail($orderdetail); + } + + //Add the price information to the orderdetail + if (!empty($partdistributor['price'])) { + $pricedetail = new Pricedetail(); + $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); + } + + //We have to flush the changes in every loop, so the find function can find newly created entities + $this->em->flush(); + } + } + /** * Returns the (parameter) unit symbol for the given ID. * @param array $data From 563d6bccd3f51e6b3c0813306d6211a49a88f96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 21:09:02 +0100 Subject: [PATCH 08/12] Added possibility to import users and projects --- .../Migrations/ImportPartKeeprCommand.php | 26 +++- src/Entity/ProjectSystem/ProjectBOMEntry.php | 2 +- .../PartKeeprImporter/PKOptionalImporter.php | 146 ++++++++++++++++++ 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index 53aecc04..22ac1ef3 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -24,10 +24,12 @@ use App\Services\ImportExportSystem\PartKeeprImporter\PKDatastructureImporter; use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter; use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper; use App\Services\ImportExportSystem\PartKeeprImporter\PKPartImporter; +use App\Services\ImportExportSystem\PartKeeprImporter\PKOptionalImporter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -41,9 +43,11 @@ class ImportPartKeeprCommand extends Command protected PKDatastructureImporter $datastructureImporter; protected PKImportHelper $importHelper; protected PKPartImporter $partImporter; + protected PKOptionalImporter $optionalImporter; public function __construct(EntityManagerInterface $em, MySQLDumpXMLConverter $xml_converter, - PKDatastructureImporter $datastructureImporter, PKPartImporter $partImporter, PKImportHelper $importHelper) + PKDatastructureImporter $datastructureImporter, PKPartImporter $partImporter, PKImportHelper $importHelper, + PKOptionalImporter $optionalImporter) { parent::__construct(self::$defaultName); $this->em = $em; @@ -51,6 +55,7 @@ class ImportPartKeeprCommand extends Command $this->importHelper = $importHelper; $this->partImporter = $partImporter; $this->xml_converter = $xml_converter; + $this->optionalImporter = $optionalImporter; } protected function configure() @@ -58,6 +63,9 @@ class ImportPartKeeprCommand extends Command $this->setDescription('Import a PartKeepr database dump into Part-DB'); $this->addArgument('file', InputArgument::REQUIRED, 'The file to which should be imported.'); + + $this->addOption('--no-projects', null, InputOption::VALUE_NONE, 'Do not import projects.'); + $this->addOption('--import-users', null, InputOption::VALUE_NONE, 'Import users (passwords will not be imported).'); } public function execute(InputInterface $input, OutputInterface $output) @@ -65,6 +73,8 @@ class ImportPartKeeprCommand extends Command $io = new SymfonyStyle($input, $output); $input_path = $input->getArgument('file'); + $no_projects_import = $input->getOption('no-projects'); + $import_users = $input->getOption('import-users'); //Make more checks here //$io->confirm('This will delete all data in the database. Do you want to continue?', false); @@ -76,9 +86,21 @@ class ImportPartKeeprCommand extends Command $xml = file_get_contents($input_path); $data = $this->xml_converter->convertMySQLDumpXMLDataToArrayStructure($xml); - //Import the data + //Import the mandatory data $this->doImport($io, $data); + if (!$no_projects_import) { + $io->info('Importing projects...'); + $count = $this->optionalImporter->importProjects($data); + $io->success('Imported '.$count.' projects.'); + } + + if ($import_users) { + $io->info('Importing users...'); + $count = $this->optionalImporter->importUsers($data); + $io->success('Imported '.$count.' users.'); + } + return 0; } diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 7fefb1fe..b48b3f50 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -58,7 +58,7 @@ class ProjectBOMEntry extends AbstractDBElement * @var string A comma separated list of the names, where this parts should be placed * @ORM\Column(type="text", name="mountnames") */ - protected string $mountnames; + protected string $mountnames = ''; /** * @var string An optional name describing this BOM entry (useful for non-part entries) diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php new file mode 100644 index 00000000..e0ac9532 --- /dev/null +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php @@ -0,0 +1,146 @@ +. + */ + +namespace App\Services\ImportExportSystem\PartKeeprImporter; + +use App\Entity\Parts\Part; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\UserSystem\Group; +use App\Entity\UserSystem\User; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * This service is used to other non mandatory data from a PartKeepr export. + * You have to import the datastructures and parts first to use project import! + */ +class PKOptionalImporter +{ + use PKImportHelperTrait; + + public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) + { + $this->em = $em; + $this->propertyAccessor = $propertyAccessor; + } + + /** + * Import the projects from the given data. + * @param array $data + * @return int The number of imported projects + */ + public function importProjects(array $data): int + { + if (!isset($data['project'])) { + throw new \RuntimeException('$data must contain a "project" key!'); + } + if (!isset($data['projectpart'])) { + throw new \RuntimeException('$data must contain a "projectpart" key!'); + } + + $projects_data = $data['project']; + $projectparts_data = $data['projectpart']; + + //First import the projects + foreach ($projects_data as $project_data) { + $project = new Project(); + $project->setName($project_data['name']); + $project->setDescription($project_data['description'] ?? ''); + + $this->setIDOfEntity($project, $project_data['id']); + $this->em->persist($project); + } + $this->em->flush(); + + //Then the project BOM entries + foreach ($projectparts_data as $projectpart_data) { + /** @var Project $project */ + $project = $this->em->find(Project::class, (int) $projectpart_data['project_id']); + if (!$project) { + throw new \RuntimeException('Could not find project with ID '.$projectpart_data['project_id']); + } + + $bom_entry = new ProjectBOMEntry(); + $bom_entry->setQuantity((float) $projectpart_data['quantity']); + $bom_entry->setName($projectpart_data['remarks']); + $this->setAssociationField($bom_entry, 'part', Part::class, $projectpart_data['part_id']); + + $comments = []; + if (!empty($projectpart_data['lotNumber'])) { + $comments[] = 'Lot number: '.$projectpart_data['lotNumber']; + } + if (!empty($projectpart_data['overage'])) { + $comments[] = 'Overage: '.$projectpart_data['overage'].($projectpart_data['overageType'] ? ' %' : ' pcs'); + } + $bom_entry->setComment(implode(',', $comments)); + + $project->addBomEntry($bom_entry); + } + $this->em->flush(); + + return count($projects_data); + } + + /** + * Import the users from the given data. + * @param array $data + * @return int The number of imported users + */ + public function importUsers(array $data): int + { + if (!isset($data['fosuser'])) { + throw new \RuntimeException('$data must contain a "fosuser" key!'); + } + + //All imported users get assigned to the "PartKeepr Users" group + $group_users = $this->em->find(Group::class, 3); + $group = $this->em->getRepository(Group::class)->findOneBy(['name' => 'PartKeepr Users', 'parent' => $group_users]); + if (!$group) { + $group = new Group(); + $group->setName('PartKeepr Users'); + $group->setParent($group_users); + $this->em->persist($group); + } + + + $users_data = $data['fosuser']; + foreach ($users_data as $user_data) { + if (in_array($user_data['username'], ['admin', 'anonymous'], true)) { + continue; + } + + $user = new User(); + $user->setName($user_data['username']); + $user->setEmail($user_data['email']); + $user->setGroup($group); + + //User is disabled by default + $user->setDisabled(true); + + //We let doctrine generate a new ID for the user + $this->em->persist($user); + } + + $this->em->flush(); + + return count($users_data); + } +} \ No newline at end of file From ae438f1650af1ecc0626653f620765bebbf8848b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 21:24:58 +0100 Subject: [PATCH 09/12] Ensure that the PartKeepr Version is correct. --- .../Migrations/ImportPartKeeprCommand.php | 6 +++++ .../PartKeeprImporter/PKImportHelper.php | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index 22ac1ef3..b81286f9 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -86,6 +86,12 @@ class ImportPartKeeprCommand extends Command $xml = file_get_contents($input_path); $data = $this->xml_converter->convertMySQLDumpXMLDataToArrayStructure($xml); + if (!$this->importHelper->checkVersion($data)) { + $db_version = $this->importHelper->getDatabaseSchemaVersion($data); + $io->error('The version of the imported database is not supported! (Version: '.$db_version.')'); + return 1; + } + //Import the mandatory data $this->doImport($io, $data); diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php index 632bcd39..3a1ae3c4 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php @@ -47,4 +47,28 @@ class PKImportHelper $purger = new ResetAutoIncrementORMPurger($this->em, ['users', '"users"', 'groups', '"groups"', 'u2f_keys', 'internal', 'migration_versions']); $purger->purge(); } + + /** + * Extracts the current database schema version from the PartKeepr XML dump. + * @param array $data + * @return string + */ + public function getDatabaseSchemaVersion(array $data): string + { + if (!isset($data['schemaversions'])) { + throw new \RuntimeException('Could not find schema version in XML dump!'); + } + + return end($data['schemaversions'])['version']; + } + + /** + * Checks that the database schema of the PartKeepr XML dump is compatible with the importer + * @param array $data + * @return bool True if the schema is compatible, false otherwise + */ + public function checkVersion(array $data): bool + { + return $this->getDatabaseSchemaVersion($data) === '20170601175559'; + } } \ No newline at end of file From bcaf8e9912329d4787a4de56056f9971e8fa51b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 22:59:31 +0100 Subject: [PATCH 10/12] Allow to import PartKeepr attachments --- .../PKDatastructureImporter.php | 20 +++- .../PartKeeprImporter/PKImportHelperTrait.php | 111 ++++++++++++++++++ .../PartKeeprImporter/PKOptionalImporter.php | 3 + .../PartKeeprImporter/PKPartImporter.php | 4 + 4 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php index 81df1c44..52732a44 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -21,6 +21,9 @@ namespace App\Services\ImportExportSystem\PartKeeprImporter; use App\Doctrine\Purger\ResetAutoIncrementORMPurger; +use App\Entity\Attachments\FootprintAttachment; +use App\Entity\Attachments\ManufacturerAttachment; +use App\Entity\Attachments\StorelocationAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\TimeStampableInterface; @@ -125,6 +128,8 @@ class PKDatastructureImporter $this->em->flush(); + $this->importAttachments($data, 'manufacturericlogo', Manufacturer::class, 'manufacturer_id', ManufacturerAttachment::class); + return count($manufacturer_data); } @@ -249,11 +254,22 @@ class PKDatastructureImporter public function importFootprints(array $data): int { - return $this->importElementsWithCategory($data, Footprint::class, 'footprint'); + $count = $this->importElementsWithCategory($data, Footprint::class, 'footprint'); + + //Footprints have both attachments and images + $this->importAttachments($data, 'footprintattachment', Footprint::class, 'footprint_id', FootprintAttachment::class); + $this->importAttachments($data, 'footprintimage', Footprint::class, 'footprint_id', FootprintAttachment::class); + + return $count; } public function importStorelocations(array $data): int { - return $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); + $count = $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); + + //Footprints have both attachments and images + $this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'footprint_id', StorelocationAttachment::class); + + return $count; } } \ No newline at end of file diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index 2d9b456d..8941502f 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -20,6 +20,9 @@ namespace App\Services\ImportExportSystem\PartKeeprImporter; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentContainingDBElement; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\TimeStampableInterface; @@ -35,6 +38,114 @@ trait PKImportHelperTrait protected EntityManagerInterface $em; protected PropertyAccessorInterface $propertyAccessor; + private ?AttachmentType $import_attachment_type = null; + + /** + * Converts a PartKeepr attachment/image row to an Attachment entity. + * @param array $attachment_row The attachment row from the PartKeepr database + * @param string $target_class The target class for the attachment + * @param string $type The type of the attachment (attachment or image) + * @return Attachment + * @throws \Exception + */ + protected function convertAttachmentDataToEntity(array $attachment_row, string $target_class, string $type): Attachment + { + //By default we use the cached version + if (!$this->import_attachment_type) { + //Get the import attachment type + $this->import_attachment_type = $this->em->getRepository(AttachmentType::class)->findOneBy([ + 'name' => 'PartKeepr Attachment' + ]); + if (!$this->import_attachment_type) { //If not existing in DB create it + $this->import_attachment_type = new AttachmentType(); + $this->import_attachment_type->setName('PartKeepr Attachment'); + $this->em->persist($this->import_attachment_type); + } + } + + if (!in_array($type, ['attachment', 'image'], true)) { + throw new \InvalidArgumentException(sprintf('The type %s is not a valid attachment type', $type)); + } + + if (!is_a($target_class, Attachment::class, true)) { + throw new \InvalidArgumentException(sprintf('The target class %s is not a subclass of %s', $target_class, Attachment::class)); + } + + /** @var Attachment $attachment */ + $attachment = new $target_class(); + if (!empty($attachment_row['description'])) { + $attachment->setName($attachment_row['description']); + } else { + $attachment->setName($attachment_row['originalname']); + } + $attachment->setFilename($attachment_row['originalname']); + $attachment->setAttachmentType($this->import_attachment_type); + $this->setCreationDate($attachment, $attachment_row['created']); + + //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); + } + + //Determine file path + //Images are stored in the (public) media folder, attachments in the (private) uploads/ folder + $path = $type === 'attachment' ? '%SECURE%' : '%MEDIA%'; + //The folder is the type of the attachment from the PartKeepr database + $path .= '/'.$attachment_row['type']; + //Next comes the filename plus extension + $path .= '/'.$attachment_row['filename'].'.'.$attachment_row['extension']; + + $attachment->setPath($path); + + return $attachment; + } + + /** + * Imports the attachments from the given data + * @param array $data The PartKeepr database + * @param string $table_name The table name for the attachments (if it contain "image", it will be treated as an image) + * @param string $target_class The target class (e.g. Part) + * @param string $target_id_field The field name where the target ID is stored + * @param string $attachment_class The attachment class (e.g. PartAttachment) + * @return void + */ + protected function importAttachments(array $data, string $table_name, string $target_class, string $target_id_field, string $attachment_class): void + { + //Determine if we have an image or an attachment + $type = str_contains($table_name, 'image') || str_contains($table_name, 'iclogo') ? 'image' : 'attachment'; + + if (!isset($data[$table_name])) { + throw new \RuntimeException(sprintf('The table %s does not exist in the PartKeepr database', $table_name)); + } + + if (!is_a($target_class, AttachmentContainingDBElement::class, true)) { + throw new \InvalidArgumentException(sprintf('The target class %s is not a subclass of %s', $target_class, AttachmentContainingDBElement::class)); + } + + if (!is_a($attachment_class, Attachment::class, true)) { + throw new \InvalidArgumentException(sprintf('The attachment class %s is not a subclass of %s', $attachment_class, Attachment::class)); + } + + //Get the table data + $table_data = $data[$table_name]; + foreach($table_data as $attachment_row) { + $attachment = $this->convertAttachmentDataToEntity($attachment_row, $attachment_class, $type); + + //Retrieve the target entity + $target_id = (int) $attachment_row[$target_id_field]; + /** @var AttachmentContainingDBElement $target */ + $target = $this->em->find($target_class, $target_id); + if (!$target) { + throw new \RuntimeException(sprintf('Could not find target entity with ID %s', $target_id)); + } + + $target->addAttachment($attachment); + $this->em->persist($attachment); + } + + $this->em->flush(); + } + /** * Assigns the parent to the given entity, using the numerical IDs from the imported data. * @param string $class diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php index e0ac9532..a19422e1 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php @@ -20,6 +20,7 @@ namespace App\Services\ImportExportSystem\PartKeeprImporter; +use App\Entity\Attachments\ProjectAttachment; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -96,6 +97,8 @@ class PKOptionalImporter } $this->em->flush(); + $this->importAttachments($data, 'projectattachment', Project::class, 'project_id', ProjectAttachment::class); + return count($projects_data); } diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index a708e997..5065d04c 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -20,6 +20,7 @@ namespace App\Services\ImportExportSystem\PartKeeprImporter; +use App\Entity\Attachments\PartAttachment; use App\Entity\Parameters\PartParameter; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -119,6 +120,9 @@ class PKPartImporter $this->importPartParameters($data); $this->importOrderdetails($data); + //Import attachments + $this->importAttachments($data, 'partattachment', Part::class, 'part_id', PartAttachment::class); + return count($part_data); } From a48b4ccaa845335029b1131f303909b64cdf745a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 25 Mar 2023 23:09:12 +0100 Subject: [PATCH 11/12] Added an check that the user really knows that the command will delete all data. --- src/Command/Migrations/ImportPartKeeprCommand.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index b81286f9..ec5010e4 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -36,7 +36,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; class ImportPartKeeprCommand extends Command { - protected static $defaultName = 'partdb:import-partkeepr'; + protected static $defaultName = 'partdb:migrations:import-partkeepr'; protected EntityManagerInterface $em; protected MySQLDumpXMLConverter $xml_converter; @@ -60,7 +60,8 @@ class ImportPartKeeprCommand extends Command protected function configure() { - $this->setDescription('Import a PartKeepr database dump into Part-DB'); + $this->setDescription('Import a PartKeepr database XML dump into Part-DB'); + $this->setHelp('This command allows you to import a PartKeepr database exported by mysqldump as XML file into Part-DB'); $this->addArgument('file', InputArgument::REQUIRED, 'The file to which should be imported.'); @@ -76,6 +77,16 @@ class ImportPartKeeprCommand extends Command $no_projects_import = $input->getOption('no-projects'); $import_users = $input->getOption('import-users'); + $io->note('This command is still in development. If you encounter any problems, please report them to the issue tracker on GitHub.'); + $io->warning('This command will delete all existing data in the database (except users). Make sure that you have no important data in the database before you continue!'); + + $io->ask('Please type "DELETE ALL DATA" to continue.', '', function ($answer) { + if (strtoupper($answer) !== 'DELETE ALL DATA') { + throw new \RuntimeException('You did not type "DELETE ALL DATA"!'); + } + return $answer; + }); + //Make more checks here //$io->confirm('This will delete all data in the database. Do you want to continue?', false); From a4e68ea2d6d1d6743611a2dcb00fb89d34e270e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 26 Mar 2023 00:32:03 +0100 Subject: [PATCH 12/12] Added documentation about PartKeepr migration process --- docs/index.md | 4 +-- docs/partkeepr_migration.md | 51 ++++++++++++++++++++++++++++++++++ docs/upgrade_legacy.md | 1 + docs/usage/console_commands.md | 1 + docs/usage/import_export.md | 2 ++ 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 docs/partkeepr_migration.md diff --git a/docs/index.md b/docs/index.md index 9bc5b1a4..42d5c516 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,7 +28,7 @@ It is installed on a web server and so can be accessed with any browser without * User system with groups and detailed (fine granular) permissions. Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped. * Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) -* Import/Export system (partial working) +* Import/Export system * Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB * Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions. * Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface. @@ -36,7 +36,7 @@ It is installed on a web server and so can be accessed with any browser without * Support for rich text descriptions and comments in parts * Support for multiple currencies and automatic update of exchange rates supported * Powerful search and filter function, including parametric search (search for parts according to some specifications) - +* Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %})) With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory, or makerspaces, where many users have should have (controlled) access to the shared inventory. diff --git a/docs/partkeepr_migration.md b/docs/partkeepr_migration.md new file mode 100644 index 00000000..41a1ff40 --- /dev/null +++ b/docs/partkeepr_migration.md @@ -0,0 +1,51 @@ +--- +layout: default +title: Migrate from PartKeepr to Part-DB +nav_order: 101 +--- + +# Migrate from PartKeepr to Part-DB + +{: .warning } +> This feature is currently in beta. Please report any bugs you find. + +This guide describes how to migrate from [PartKeepr](https://partkeepr.org/) to Part-DB. + +Part-DB has a built-in migration tool, which can be used to migrate the data from an existing PartKeepr instance to +a new Part-DB instance. Most of the data can be migrated, however there are some limitations, you can find below. + +## What can be imported +* Datastructures (Categories, Footprints, Storage Locations, Manufacturers, Distributors, Part Measurement Units) +* Basic part informations (Name, Description, Comment, etc.) +* Attachments and images of parts, projects, footprints, manufacturers and storage locations +* Part prices (distributor infos) +* Part parameters +* Projects (including parts and attachments) +* Users (optional): Passwords however will be not migrated, and need to be reset later + +## What can't be imported +* Metaparts (A dummy version of the metapart will be created in Part-DB, however it will not function as metapart) +* Multiple manufacturers per part (only the last manufacturer of a part will be migrated) +* Overage information for project parts (the overage info will be set as comment in the project BOM, but will have no effect) +* Batch Jobs +* Parameter Units (the units will be written into the parameters) +* Project Reports and Project Runs +* Stock history +* Any kind of PartKeepr preferences + +## How to migrate +1. Install Part-DB like described in the installation guide. You can use any database backend you want (mysql or sqlite). Run the database migration, but do not create any new data yet. +2. Export your PartKeepr database as XML file using [mysqldump](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html): +When the MySQL database is running on the local computer and you are root you can just run the command `mysqldump --xml PARTKEEPR_DATABASE --result-file pk.xml`. +If your server is remote or your MySQL authentication is different, you need to run `mysqldump --xml -h PARTKEEPR_HOST -u PARTKEEPR_USER -p PARTKEEPR_DATABASE`, where you replace `PARTKEEPR_HOST` +with the hostname of your MySQL database and `PARTKEEPR_USER` with the username of MySQL user which has access to the PartKeepr database. You will be asked for the MySQL user password. +3. Go the Part-DB main folder and run the command `php bin/console partdb:migrations:import-partkeepr path/to/pk.xml`. This step will delete all existing data in the Part-DB database and import the contents of PartKeepr. +4. Copy the contents of `data/files/` from your PartKeepr installation to the `uploads/` folder of your Part-DB installation and the contents of `data/images` from PartKeepr to `public/media/` of Part-DB. +5. Clear the cache of Part-DB by running: `php bin/console cache:clear` +6. Go to the Part-DB web interface. You can login with the username `admin` and the password, which is shown during the installation process of Part-DB (step 1). You should be able to see all the data from PartKeepr. + +## Import users +If you want to import the users (mostly the username and email address) from PartKeepr, you can add the `--import-users` option on the database import command (step 3): `php bin/console partdb:migrations:import-partkeepr --import-users path/to/pk.xml`. + +All imported users of PartKeepr will be assigned to a new group "PartKeepr Users", which has normal user permissions (so editing data, but no administrative tasks). You can change the group and permissions later in Part-DB users managment. +Passwords can not be imported from PartKeepr and all imported users get marked as disabled user. So to allow users to login, you need to enable them in the user management and assign a password. \ No newline at end of file diff --git a/docs/upgrade_legacy.md b/docs/upgrade_legacy.md index ca9ee053..d43fa2ed 100644 --- a/docs/upgrade_legacy.md +++ b/docs/upgrade_legacy.md @@ -1,6 +1,7 @@ --- layout: default title: Upgrade from legacy Part-DB version (<1.0) +nav_order: 100 --- # Upgrade from legacy Part-DB version diff --git a/docs/usage/console_commands.md b/docs/usage/console_commands.md index f762f614..7a23483a 100644 --- a/docs/usage/console_commands.md +++ b/docs/usage/console_commands.md @@ -31,6 +31,7 @@ You can get help for every command with the parameter `--help`. See `php bin/con * `partdb:migrations:convert-bbcode`: Migrate the old BBCode markup codes used in legacy Part-DB versions (< 1.0.0) to the new markdown syntax * `partdb:attachments:clean-unused`: Remove all attachments which are not used by any database entry (e.g. orphaned attachments) * `partdb:cache:clear`: Clears all caches, so the next page load will be slower, but the cache will be rebuild. This can maybe fix some issues, when the cache were corrupted. This command is also needed after changing things in the `parameters.yaml` file or upgrading Part-DB. +* `partdb:migrations:import-partkeepr`: Imports an mysqldump XML dump of a PartKeepr database into Part-DB. This is only needed for users, which want to migrate from PartKeepr to Part-DB. *All existing data in the Part-DB database is deleted!* ## Database commands * `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version diff --git a/docs/usage/import_export.md b/docs/usage/import_export.md index c16f5767..09e4b163 100644 --- a/docs/usage/import_export.md +++ b/docs/usage/import_export.md @@ -16,6 +16,8 @@ Part-DB offers the possibility to import existing data (parts, datastructures, e > administrators. If you want to allow other users to import data, or can not import data, check the permissions of the user. You can enable import for each data structure > individually in the permissions settings. +If you want to import data from PartKeepr you might want to look into the [PartKeepr migration guide]({% link upgrade_legacy.md %}). + ### Import parts Part-DB supports the import of parts from CSV files and other formats. This can be used to import existing parts from other databases or datasources into Part-DB. The import can be done via the "Tools -> Import parts" page, which you can find in the "Tools" sidebar panel.