mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-02 15:39:35 +00:00
add supplier SPN linking for BOM import (#1209)
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / docker (push) Waiting to run
Docker Image Build (FrankenPHP) / docker (push) Waiting to run
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
* feat: add supplier SPN lookup for BOM import Add automatic part linking via supplier part numbers (SPNs) in the BOM importer. When a Part-DB ID is not provided, the importer now searches for existing parts by matching supplier SPNs from the CSV with orderdetail records in the database. This allows automatic part linking when KiCad schematic BOMs contain supplier information like LCSC SPN, Mouser SPN, etc., improving the import workflow for users who track parts by supplier part numbers. * add tests for BOM import with supplier SPN handling
This commit is contained in:
parent
584643d4ca
commit
a355bda9da
2 changed files with 214 additions and 1 deletions
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = <<<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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue