diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 33a402cb..8a91c825 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -277,8 +277,11 @@ class BOMImporter // Fetch suppliers once for efficiency $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); $supplierSPNKeys = []; + $suppliersByName = []; // Map supplier names to supplier objects foreach ($suppliers as $supplier) { - $supplierSPNKeys[] = $supplier->getName() . ' SPN'; + $supplierName = $supplier->getName(); + $supplierSPNKeys[] = $supplierName . ' SPN'; + $suppliersByName[$supplierName] = $supplier; } foreach ($csv->getRecords() as $offset => $entry) { @@ -356,6 +359,41 @@ class BOMImporter } } + // Try to link existing part based on supplier part number if no Part-DB ID is given + if ($part === null) { + // Check all available supplier SPN fields + foreach ($suppliersByName as $supplierName => $supplier) { + $supplier_spn = null; + + if (isset($mapped_entry[$supplierName . ' SPN']) && !empty(trim($mapped_entry[$supplierName . ' SPN']))) { + $supplier_spn = trim($mapped_entry[$supplierName . ' SPN']); + } + + if ($supplier_spn !== null) { + // Query for orderdetails with matching supplier and SPN + $orderdetail = $this->entityManager->getRepository(\App\Entity\PriceInformations\Orderdetail::class) + ->findOneBy([ + 'supplier' => $supplier, + 'supplierpartnr' => $supplier_spn, + ]); + + if ($orderdetail !== null && $orderdetail->getPart() !== null) { + $part = $orderdetail->getPart(); + $name = $part->getName(); // Update name with actual part name + + $this->logger->info('Linked BOM entry to existing part via supplier SPN', [ + 'supplier' => $supplierName, + 'supplier_spn' => $supplier_spn, + 'part_id' => $part->getID(), + 'part_name' => $part->getName(), + ]); + + break; // Stop searching once a match is found + } + } + } + } + // Create unique key for this entry (name + part ID) $entry_key = $name . '|' . ($part ? $part->getID() : 'null'); diff --git a/tests/Services/ImportExportSystem/BOMImporterTest.php b/tests/Services/ImportExportSystem/BOMImporterTest.php index 47ddcc24..a8841f17 100644 --- a/tests/Services/ImportExportSystem/BOMImporterTest.php +++ b/tests/Services/ImportExportSystem/BOMImporterTest.php @@ -616,6 +616,181 @@ class BOMImporterTest extends WebTestCase $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); } + public function testStringToBOMEntriesKiCADSchematicWithSupplierSPN(): void + { + // Create test supplier + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $this->entityManager->persist($lcscSupplier); + + // Create a test part with required fields + $part = new Part(); + $part->setName('Test Resistor 10k 0805'); + $part->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part); + + // Create orderdetail linking the part to a supplier SPN + $orderdetail = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail->setPart($part); + $orderdetail->setSupplier($lcscSupplier); + $orderdetail->setSupplierpartnr('C123456'); + $this->entityManager->persist($orderdetail); + + $this->entityManager->flush(); + + // Import CSV with LCSC SPN matching the orderdetail + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); + + // Verify that the BOM entry is linked to the correct part via supplier SPN + $this->assertSame($part, $bom_entries[0]->getPart()); + $this->assertEquals('Test Resistor 10k 0805', $bom_entries[0]->getName()); + $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); + $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertStringContainsString('LCSC SPN: C123456', $bom_entries[0]->getComment()); + $this->assertStringContainsString('Part-DB ID: ' . $part->getID(), $bom_entries[0]->getComment()); + + // Clean up + $this->entityManager->remove($orderdetail); + $this->entityManager->remove($part); + $this->entityManager->remove($lcscSupplier); + $this->entityManager->flush(); + } + + public function testStringToBOMEntriesKiCADSchematicWithMultipleSupplierSPNs(): void + { + // Create test suppliers + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $mouserSupplier = new Supplier(); + $mouserSupplier->setName('Mouser'); + $this->entityManager->persist($lcscSupplier); + $this->entityManager->persist($mouserSupplier); + + // Create first part linked via LCSC SPN + $part1 = new Part(); + $part1->setName('Resistor 10k'); + $part1->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part1); + + $orderdetail1 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail1->setPart($part1); + $orderdetail1->setSupplier($lcscSupplier); + $orderdetail1->setSupplierpartnr('C123456'); + $this->entityManager->persist($orderdetail1); + + // Create second part linked via Mouser SPN + $part2 = new Part(); + $part2->setName('Capacitor 100nF'); + $part2->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part2); + + $orderdetail2 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail2->setPart($part2); + $orderdetail2->setSupplier($mouserSupplier); + $orderdetail2->setSupplierpartnr('789-CAP100NF'); + $this->entityManager->persist($orderdetail2); + + $this->entityManager->flush(); + + // Import CSV with both LCSC and Mouser SPNs + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Mouser SPN' => 'Mouser SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertCount(2, $bom_entries); + + // Verify first entry linked via LCSC SPN + $this->assertSame($part1, $bom_entries[0]->getPart()); + $this->assertEquals('Resistor 10k', $bom_entries[0]->getName()); + + // Verify second entry linked via Mouser SPN + $this->assertSame($part2, $bom_entries[1]->getPart()); + $this->assertEquals('Capacitor 100nF', $bom_entries[1]->getName()); + + // Clean up + $this->entityManager->remove($orderdetail1); + $this->entityManager->remove($orderdetail2); + $this->entityManager->remove($part1); + $this->entityManager->remove($part2); + $this->entityManager->remove($lcscSupplier); + $this->entityManager->remove($mouserSupplier); + $this->entityManager->flush(); + } + + public function testStringToBOMEntriesKiCADSchematicWithNonMatchingSPN(): void + { + // Create test supplier + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $this->entityManager->persist($lcscSupplier); + $this->entityManager->flush(); + + // Import CSV with LCSC SPN that doesn't match any orderdetail + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertCount(1, $bom_entries); + + // Verify that no part is linked (SPN not found) + $this->assertNull($bom_entries[0]->getPart()); + $this->assertEquals('10k', $bom_entries[0]->getName()); // Should use Value as name + $this->assertStringContainsString('LCSC SPN: C999999', $bom_entries[0]->getComment()); + + // Clean up + $this->entityManager->remove($lcscSupplier); + $this->entityManager->flush(); + } + private function getDefaultCategory(EntityManagerInterface $entityManager) { // Get the first available category or create a default one