. */ 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\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use App\Settings\SystemSettings\LocalizationSettings; use Brick\Math\BigDecimal; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Intl\Currencies; 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, private readonly LocalizationSettings $localizationSettings) { $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((float) ($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']); } $this->setAssociationField($entity, 'partCustomState', MeasurementUnit::class, $part['partCustomState_id']); //Create a part lot to store the stock level and location $lot = new PartLot(); $lot->setAmount((float) ($part['stockLevel'] ?? 0)); $this->setAssociationField($lot, 'storage_location', StorageLocation::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 is_countable($part_data) ? count($part_data) : 0; } 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']); //The manufacturer is optional if (!$manufacturer instanceof Manufacturer && !empty($partmanufacturer['manufacturer_id'])) { 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'] ?? ''); if ($partparameter['unit_id'] !== null && (int) $partparameter['unit_id'] !== 0) { $entity->setUnit($this->getUnitSymbol($data, (int)$partparameter['unit_id'])); } else { $entity->setUnit(""); } if ($partparameter['value'] !== null) { $entity->setValueTypical((float) $partparameter['value'] * $this->getSIPrefixFactor($data, (int) $partparameter['siPrefix_id'])); } if ($partparameter['minimumValue'] !== null) { $entity->setValueMin((float) $partparameter['minimumValue'] * $this->getSIPrefixFactor($data, (int) $partparameter['minSiPrefix_id'])); } if ($partparameter['maximumValue'] !== null) { $entity->setValueMax((float) $partparameter['maximumValue'] * $this->getSIPrefixFactor($data, (int) $partparameter['maxSiPrefix_id'])); } $part = $this->em->find(Part::class, (int) $partparameter['part_id']); if (!$part instanceof 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 currency for the given ISO code. If the currency does not exist, it is created. * This function returns null if the ISO code is the base currency. */ protected function getOrCreateCurrency(string $currency_iso_code): ?Currency { //Normalize ISO code $currency_iso_code = strtoupper($currency_iso_code); //We do not have a currency for the base currency to be consistent with prices without currencies if ($currency_iso_code === $this->localizationSettings->baseCurrency) { return null; } $currency = $this->em->getRepository(Currency::class)->findOneBy([ 'iso_code' => $currency_iso_code, ]); if ($currency === null) { $currency = new Currency(); $currency->setIsoCode($currency_iso_code); $currency->setName(Currencies::getName($currency_iso_code)); $this->em->persist($currency); $this->em->flush(); } return $currency; } protected function importOrderdetails(array $data): void { if (!isset($data['partdistributor'])) { 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 instanceof 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 instanceof 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 = ''; } $orderdetail = $this->em->getRepository(Orderdetail::class)->findOneBy([ 'part' => $part, 'supplier' => $supplier, 'supplierpartnr' => $spn, ]); //When no orderdetail exists, create one if ($orderdetail === null) { $orderdetail = new Orderdetail(); $orderdetail->setSupplier($supplier); $orderdetail->setSupplierpartnr($spn); $part->addOrderdetail($orderdetail); $this->em->persist($orderdetail); } //Add the price information to the orderdetail (only if the price is not zero, as this was a placeholder in PartKeepr) if (!empty($partdistributor['price']) && !BigDecimal::of($partdistributor['price'])->isZero()) { $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']); $packaging_unit = (float) ($partdistributor['packagingUnit'] ?? 1); $pricedetail->setPrice($price_per_item->multipliedBy($packaging_unit)); $pricedetail->setPriceRelatedQuantity($packaging_unit); //We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept) //But in Part-DB the minimum discount qty have to be unique across a orderdetail $pricedetail->setMinDiscountQuantity($packaging_unit); //Set the currency of the price if (!empty($partdistributor['currency'])) { $currency = $this->getOrCreateCurrency($partdistributor['currency']); $pricedetail->setCurrency($currency); } $this->em->persist($pricedetail); } $this->em->flush(); //Clear the entity manager to improve performance $this->em->clear(); } } /** * Returns the (parameter) unit symbol for the given ID. */ 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)); } }