Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/Services/ImportExportSystem/BOMImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,11 @@ private function parseKiCADSchematic(string $data, array $options = []): array
// 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) {
Expand Down Expand Up @@ -356,6 +359,41 @@ private function parseKiCADSchematic(string $data, array $options = []): array
}
}

// 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');

Expand Down
175 changes: 175 additions & 0 deletions tests/Services/ImportExportSystem/BOMImporterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,181 @@ public function testStringToBOMEntriesKiCADSchematicWithBOM(): void
$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 = <<<CSV
"Reference","Value","LCSC SPN","Quantity"
"R1,R2","10k","C123456","2"
CSV;

$field_mapping = [
'Reference' => '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 = <<<CSV
"Reference","Value","LCSC SPN","Mouser SPN","Quantity"
"R1","10k","C123456","","1"
"C1","100nF","","789-CAP100NF","1"
CSV;

$field_mapping = [
'Reference' => '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 = <<<CSV
"Reference","Value","LCSC SPN","Quantity"
"R1","10k","C999999","1"
CSV;

$field_mapping = [
'Reference' => '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
Expand Down
Loading