mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-23 12:24:45 +02:00
Merge branch 'partkeepr_import'
This commit is contained in:
commit
0ae57b8b7b
18 changed files with 10329 additions and 6 deletions
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
51
docs/partkeepr_migration.md
Normal file
51
docs/partkeepr_migration.md
Normal file
|
@ -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.
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
layout: default
|
||||
title: Upgrade from legacy Part-DB version (<1.0)
|
||||
nav_order: 100
|
||||
---
|
||||
|
||||
# Upgrade from legacy Part-DB version
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
158
src/Command/Migrations/ImportPartKeeprCommand.php
Normal file
158
src/Command/Migrations/ImportPartKeeprCommand.php
Normal file
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Command\Migrations;
|
||||
|
||||
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;
|
||||
|
||||
class ImportPartKeeprCommand extends Command
|
||||
{
|
||||
|
||||
protected static $defaultName = 'partdb:migrations:import-partkeepr';
|
||||
|
||||
protected EntityManagerInterface $em;
|
||||
protected MySQLDumpXMLConverter $xml_converter;
|
||||
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,
|
||||
PKOptionalImporter $optionalImporter)
|
||||
{
|
||||
parent::__construct(self::$defaultName);
|
||||
$this->em = $em;
|
||||
$this->datastructureImporter = $datastructureImporter;
|
||||
$this->importHelper = $importHelper;
|
||||
$this->partImporter = $partImporter;
|
||||
$this->xml_converter = $xml_converter;
|
||||
$this->optionalImporter = $optionalImporter;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$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.');
|
||||
|
||||
$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)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$input_path = $input->getArgument('file');
|
||||
$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);
|
||||
|
||||
//Purge the databse, so we will not have any conflicts
|
||||
$this->importHelper->purgeDatabaseForImport();
|
||||
|
||||
//Convert the XML file to an array
|
||||
$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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private function doImport(SymfonyStyle $io, array $data): void
|
||||
{
|
||||
//First import the distributors
|
||||
$io->info('Importing distributors...');
|
||||
$count = $this->datastructureImporter->importDistributors($data);
|
||||
$io->success('Imported '.$count.' distributors.');
|
||||
|
||||
//Import the measurement units
|
||||
$io->info('Importing part measurement units...');
|
||||
$count = $this->datastructureImporter->importPartUnits($data);
|
||||
$io->success('Imported '.$count.' measurement units.');
|
||||
|
||||
//Import manufacturers
|
||||
$io->info('Importing manufacturers...');
|
||||
$count = $this->datastructureImporter->importManufacturers($data);
|
||||
$io->success('Imported '.$count.' manufacturers.');
|
||||
|
||||
$io->info('Importing categories...');
|
||||
$count = $this->datastructureImporter->importCategories($data);
|
||||
$io->success('Imported '.$count.' categories.');
|
||||
|
||||
$io->info('Importing Footprints...');
|
||||
$count = $this->datastructureImporter->importFootprints($data);
|
||||
$io->success('Imported '.$count.' footprints.');
|
||||
|
||||
$io->info('Importing storage locations...');
|
||||
$count = $this->datastructureImporter->importStorelocations($data);
|
||||
$io->success('Imported '.$count.' storage locations.');
|
||||
|
||||
$io->info('Importing parts...');
|
||||
$count = $this->partImporter->importParts($data);
|
||||
$io->success('Imported '.$count.' parts.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\ImportExportSystem\PartKeeprImporter;
|
||||
|
||||
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 <mysqldump> 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 <database> 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 <database> node
|
||||
$database = $databases->item(0);
|
||||
|
||||
//Get all <table_data> nodes
|
||||
$tables = $database->getElementsByTagName('table_data');
|
||||
$table_data = [];
|
||||
|
||||
//Iterate over all <table> 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 <row> nodes
|
||||
$rows = $table->getElementsByTagName('row');
|
||||
|
||||
//Iterate over all <row> 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 <field> nodes
|
||||
$fields = $table_row->getElementsByTagName('field');
|
||||
|
||||
//Iterate over all <field> nodes
|
||||
foreach ($fields as $field) {
|
||||
$row_data[$field->getAttribute('name')] = $field->nodeValue;
|
||||
}
|
||||
|
||||
return $row_data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\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;
|
||||
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();
|
||||
|
||||
$this->importAttachments($data, 'manufacturericlogo', Manufacturer::class, 'manufacturer_id', ManufacturerAttachment::class);
|
||||
|
||||
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
|
||||
{
|
||||
$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
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\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;
|
||||
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;
|
||||
|
||||
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
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\ImportExportSystem\PartKeeprImporter;
|
||||
|
||||
use App\Entity\Attachments\ProjectAttachment;
|
||||
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();
|
||||
|
||||
$this->importAttachments($data, 'projectattachment', Project::class, 'project_id', ProjectAttachment::class);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\ImportExportSystem\PartKeeprImporter;
|
||||
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
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 App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use Brick\Math\BigDecimal;
|
||||
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);
|
||||
$this->importOrderdetails($data);
|
||||
|
||||
//Import attachments
|
||||
$this->importAttachments($data, 'partattachment', Part::class, 'part_id', PartAttachment::class);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
* @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));
|
||||
}
|
||||
}
|
54
tests/Services/Misc/MySQLDumpXMLConverterTest.php
Normal file
54
tests/Services/Misc/MySQLDumpXMLConverterTest.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Services\Misc;
|
||||
|
||||
use App\Services\ImportExportSystem\PartKeeprImporter\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']);
|
||||
|
||||
}
|
||||
}
|
8952
tests/assets/partkeepr_import_test.xml
Normal file
8952
tests/assets/partkeepr_import_test.xml
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue