"""Tests barcode parsing for all suppliers.""" from django.urls import reverse from company.models import Company, ManufacturerPart, SupplierPart from InvenTree.unit_test import InvenTreeAPITestCase from order.models import PurchaseOrder, PurchaseOrderLineItem from part.models import Part from plugin.registry import registry from stock.models import StockItem, StockLocation class SupplierBarcodeTests(InvenTreeAPITestCase): """Tests barcode parsing for all suppliers.""" SCAN_URL = reverse('api-barcode-scan') roles = [ 'stock.view', 'stock_location.view', 'part.view', 'company.view', 'order.view', ] @classmethod def setUpTestData(cls): """Create supplier parts for barcodes.""" super().setUpTestData() # Enable the plugins registry.set_plugin_state('digikeyplugin', True) registry.set_plugin_state('mouserplugin', True) registry.set_plugin_state('lcscplugin', True) registry.set_plugin_state('tmeplugin', True) part = Part.objects.create(name='Test Part', description='Test Part') manufacturer = Company.objects.create( name='Test Manufacturer', is_manufacturer=True ) mpart1 = ManufacturerPart.objects.create( part=part, manufacturer=manufacturer, MPN='MC34063ADR' ) mpart2 = ManufacturerPart.objects.create( part=part, manufacturer=manufacturer, MPN='LDK320ADU33R' ) digikey_supplier = Company.objects.create(name='Supplier', is_supplier=True) mouser_supplier = Company.objects.create(name='Mouser Test', is_supplier=True) supplier_parts = [ SupplierPart( SKU='296-LM358BIDDFRCT-ND', part=part, supplier=digikey_supplier ), SupplierPart(SKU='C312270', part=part, supplier=digikey_supplier), SupplierPart(SKU='WBP-302', part=part, supplier=digikey_supplier), SupplierPart( SKU='1', part=part, manufacturer_part=mpart1, supplier=mouser_supplier ), SupplierPart( SKU='2', part=part, manufacturer_part=mpart2, supplier=mouser_supplier ), ] SupplierPart.objects.bulk_create(supplier_parts) # Assign supplier information to the plugins # Add supplier information to each custom plugin registry.set_plugin_state('digikeyplugin', True) digikey_plugin = registry.get_plugin('digikeyplugin') digikey_plugin.set_setting('SUPPLIER_ID', digikey_supplier.pk) registry.set_plugin_state('mouserplugin', True) mouser_plugin = registry.get_plugin('mouserplugin') mouser_plugin.set_setting('SUPPLIER_ID', mouser_supplier.pk) def test_digikey_barcode(self): """Test digikey barcode.""" result = self.post( self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE}, expected_code=200 ) self.assertEqual(result.data['plugin'], 'DigiKeyBarcodePlugin') supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND') def test_digikey_2_barcode(self): """Test digikey barcode which uses 30P instead of P.""" result = self.post( self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_2}, expected_code=200 ) self.assertEqual(result.data['plugin'], 'DigiKeyBarcodePlugin') supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND') def test_digikey_3_barcode(self): """Test digikey barcode which is invalid.""" self.post(self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_3}, expected_code=400) def test_mouser_barcode(self): """Test mouser barcode with custom order number.""" result = self.post( self.SCAN_URL, data={'barcode': MOUSER_BARCODE}, expected_code=200 ) supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, '1') def test_old_mouser_barcode(self): """Test old mouser barcode with messed up header.""" registry.set_plugin_state('mouserplugin', False) # Initial scan should fail - plugin not enabled self.post( self.SCAN_URL, data={'barcode': MOUSER_BARCODE_OLD}, expected_code=400 ) registry.set_plugin_state('mouserplugin', True) result = self.post( self.SCAN_URL, data={'barcode': MOUSER_BARCODE_OLD}, expected_code=200 ) supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, '2') def test_lcsc_barcode(self): """Test LCSC barcode.""" result = self.post( self.SCAN_URL, data={'barcode': LCSC_BARCODE}, expected_code=200 ) self.assertEqual(result.data['plugin'], 'LCSCBarcodePlugin') supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, 'C312270') def test_tme_qrcode(self): """Test TME QR-Code.""" result = self.post( self.SCAN_URL, data={'barcode': TME_QRCODE}, expected_code=200 ) self.assertEqual(result.data['plugin'], 'TMEBarcodePlugin') supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, 'WBP-302') def test_tme_barcode2d(self): """Test TME DataMatrix-Code.""" result = self.post( self.SCAN_URL, data={'barcode': TME_DATAMATRIX_CODE}, expected_code=200 ) self.assertEqual(result.data['plugin'], 'TMEBarcodePlugin') supplier_part_data = result.data.get('supplierpart') self.assertIn('pk', supplier_part_data) supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk']) self.assertEqual(supplier_part.SKU, 'WBP-302') class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase): """Tests barcode scanning to receive a purchase order item.""" roles = ['stock.view', 'stock_location.view'] def setUp(self): """Create supplier part and purchase_order.""" super().setUp() registry.set_plugin_state('digikeyplugin', True) registry.set_plugin_state('mouserplugin', True) registry.set_plugin_state('lcscplugin', True) registry.set_plugin_state('tmeplugin', True) self.loc_1 = StockLocation.objects.create(name='Location 1') self.loc_2 = StockLocation.objects.create(name='Location 2') part = Part.objects.create(name='Test Part', description='Test Part') digikey_supplier = Company.objects.create(name='Supplier', is_supplier=True) manufacturer = Company.objects.create( name='Test Manufacturer', is_manufacturer=True ) mouser = Company.objects.create(name='Mouser Test', is_supplier=True) mpart = ManufacturerPart.objects.create( part=part, manufacturer=manufacturer, MPN='MC34063ADR' ) self.purchase_order1 = PurchaseOrder.objects.create( supplier_reference='72991337', supplier=digikey_supplier, destination=self.loc_1, ) supplier_parts1 = [ SupplierPart(SKU=f'1_{i}', part=part, supplier=digikey_supplier) for i in range(6) ] supplier_parts1.insert( 2, SupplierPart( SKU='296-LM358BIDDFRCT-ND', part=part, supplier=digikey_supplier ), ) for supplier_part in supplier_parts1: supplier_part.save() self.purchase_order1.add_line_item(supplier_part, 8, destination=self.loc_2) self.purchase_order2 = PurchaseOrder.objects.create( reference='P0-1337', supplier=mouser, destination=self.loc_1 ) self.purchase_order2.place_order() supplier_parts2 = [ SupplierPart(SKU=f'2_{i}', part=part, supplier=mouser) for i in range(6) ] supplier_parts2.insert( 3, SupplierPart(SKU='42', part=part, manufacturer_part=mpart, supplier=mouser), ) for supplier_part in supplier_parts2: supplier_part.save() self.purchase_order2.add_line_item(supplier_part, 5, destination=self.loc_2) # Add supplier information to each custom plugin registry.set_plugin_state('digikeyplugin', True) digikey_plugin = registry.get_plugin('digikeyplugin') digikey_plugin.set_setting('SUPPLIER_ID', digikey_supplier.pk) registry.set_plugin_state('mouserplugin', True) mouser_plugin = registry.get_plugin('mouserplugin') mouser_plugin.set_setting('SUPPLIER_ID', mouser.pk) def test_receive(self): """Test receiving an item from a barcode.""" url = reverse('api-barcode-po-receive') # First attempt - PO is not yet "placed" result1 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400) self.assertIn('received against an order marked as', result1.data['error']) # Next, place the order - receipt should then work self.purchase_order1.place_order() result2 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200) self.assertIn('success', result2.data) # Attempt to receive the same item again # Already received - failure expected result3 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400) self.assertEqual(result3.data['error'], 'Item has already been received') result5 = self.post( reverse('api-barcode-scan'), data={'barcode': DIGIKEY_BARCODE}, expected_code=200, ) stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk']) self.assertEqual(stock_item.supplier_part.SKU, '296-LM358BIDDFRCT-ND') self.assertEqual(stock_item.quantity, 10) self.assertEqual(stock_item.location, self.loc_2) def test_no_auto_allocate(self): """Test with auto_allocate explicitly disabled.""" url = reverse('api-barcode-po-receive') self.purchase_order1.place_order() response = self.post( url, data={'barcode': DIGIKEY_BARCODE, 'auto_allocate': False}, expected_code=200, ) self.assertEqual(response.data['plugin'], 'DigiKeyBarcodePlugin') self.assertIn('action_required', response.data) item = response.data['lineitem'] self.assertEqual(item['quantity'], 10.0) self.assertEqual(item['purchase_order'], self.purchase_order1.pk) self.assertEqual(item['location'], self.loc_2.pk) def test_receive_custom_order_number(self): """Test receiving an item from a barcode with a custom order number.""" url = reverse('api-barcode-po-receive') result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200) self.assertIn('success', result1.data) # Scan the same barcode again - should be resolved to the created item result2 = self.post( reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}, expected_code=200, ) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) self.assertEqual(stock_item.supplier_part.SKU, '42') self.assertEqual(stock_item.supplier_part.manufacturer_part.MPN, 'MC34063ADR') self.assertEqual(stock_item.quantity, 3) self.assertEqual(stock_item.location, self.loc_2) self.assertEqual(stock_item.barcode_data, MOUSER_BARCODE) def test_receive_stock_location(self): """Test receiving an item when the location is provided.""" stock_location = StockLocation.objects.create(name='Test Location') url = reverse('api-barcode-po-receive') result1 = self.post( url, data={'barcode': MOUSER_BARCODE, 'location': stock_location.pk}, expected_code=200, ) self.assertIn('success', result1.data) result2 = self.post( reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}, expected_code=200, ) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) self.assertEqual(stock_item.location, stock_location) def test_receive_default_line_item_location(self): """Test receiving an item into the default line_item location.""" StockLocation.objects.create(name='Test Location 1') stock_location2 = StockLocation.objects.create(name='Test Location 2') line_item = PurchaseOrderLineItem.objects.filter(part__SKU='42')[0] line_item.destination = stock_location2 line_item.save() url = reverse('api-barcode-po-receive') result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200) self.assertIn('success', result1.data) result2 = self.post( reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}, expected_code=200, ) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) self.assertEqual(stock_item.location, stock_location2) def test_receive_default_part_location(self): """Test receiving an item into the default part location.""" StockLocation.objects.create(name='Test Location 1') stock_location2 = StockLocation.objects.create(name='Test Location 2') # Ensure no other fallback locations are set # This is to ensure that the part location is used instead self.purchase_order2.destination = None self.purchase_order2.save() for line in self.purchase_order2.lines.all(): line.destination = None line.save() part = Part.objects.all()[0] part.default_location = stock_location2 part.save() url = reverse('api-barcode-po-receive') result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200) self.assertIn('success', result1.data) result2 = self.post( reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}, expected_code=200, ) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) self.assertEqual(stock_item.location, stock_location2) def test_receive_specific_order_and_location(self): """Test receiving an item from a specific order into a specific location.""" StockLocation.objects.create(name='Test Location 1') stock_location2 = StockLocation.objects.create(name='Test Location 2') url = reverse('api-barcode-po-receive') barcode = MOUSER_BARCODE.replace('\x1dKP0-1337', '') result1 = self.post( url, data={ 'barcode': barcode, 'purchase_order': self.purchase_order2.pk, 'location': stock_location2.pk, }, expected_code=200, ) self.assertIn('success', result1.data) result2 = self.post( reverse('api-barcode-scan'), data={'barcode': barcode}, expected_code=200 ) stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk']) self.assertEqual(stock_item.location, stock_location2) def test_receive_missing_quantity(self): """Test receiving an with missing quantity information.""" url = reverse('api-barcode-po-receive') barcode = MOUSER_BARCODE.replace('\x1dQ3', '') response = self.post(url, data={'barcode': barcode}, expected_code=200) self.assertIn('action_required', response.data) self.assertIn('lineitem', response.data) # Quantity should be pre-filled with the remaining quantity self.assertEqual(5, response.data['lineitem']['quantity']) def test_receive_includes_no_match_false(self): """Successful receive merges no_match=False from the plugin response.""" self.purchase_order1.place_order() url = reverse('api-barcode-po-receive') result = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200) self.assertIn('success', result.data) self.assertIn('no_match', result.data) self.assertFalse(result.data['no_match']) def test_partial_match_po_found_no_supplier_part(self): """Barcode references a known PO but the SKU has no matching supplier part.""" url = reverse('api-barcode-po-receive') # '1K72991337' matches purchase_order1 via supplier_reference; SKU is unknown result = self.post( url, data={'barcode': DIGIKEY_BARCODE_PO_NO_PART}, expected_code=400 ) self.assertIn('error', result.data) self.assertIn('Purchase order Found', result.data['error']) self.assertIn('No supplier Part Match', result.data['error']) self.assertIn('supplier_matches', result.data) matches = result.data['supplier_matches'] self.assertIsNotNone(matches['purchase_order']) # DRF's ValidationError converts None leaves to ErrorDetail('None') self.assertEqual(str(matches['supplier_part']), 'None') self.assertIsNotNone(matches['supplier']) self.assertTrue(matches['no_match']) def test_partial_match_supplier_part_found_no_po(self): """Barcode references a known supplier part but the PO reference has no match.""" url = reverse('api-barcode-po-receive') # 'P296-LM358BIDDFRCT-ND' matches the existing supplier part; PO ref is unknown result = self.post( url, data={'barcode': DIGIKEY_BARCODE_PART_NO_PO}, expected_code=400 ) self.assertIn('error', result.data) self.assertIn('Supplier Part Found', result.data['error']) self.assertIn('No Purchase Order Match', result.data['error']) self.assertIn('supplier_matches', result.data) matches = result.data['supplier_matches'] # DRF's ValidationError converts None leaves to ErrorDetail('None') self.assertEqual(str(matches['purchase_order']), 'None') self.assertIsNotNone(matches['supplier_part']) self.assertIsNotNone(matches['supplier']) self.assertTrue(matches['no_match']) def test_no_supplier_plugin_error(self): """Plugins that cannot resolve a supplier are listed in no_supplier_plugin_error.""" url = reverse('api-barcode-po-receive') digikey_plugin = registry.get_plugin('digikeyplugin') original_supplier_id = digikey_plugin.get_setting('SUPPLIER_ID') # Use a non-existent PK so get_supplier() returns None digikey_plugin.set_setting('SUPPLIER_ID', 99999) if hasattr(digikey_plugin, '_supplier'): del digikey_plugin._supplier try: result = self.post( url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400 ) self.assertIn('no_supplier_plugin_error', result.data) self.assertIn('digikeyplugin', result.data['no_supplier_plugin_error']) finally: digikey_plugin.set_setting('SUPPLIER_ID', original_supplier_id) if hasattr(digikey_plugin, '_supplier'): del digikey_plugin._supplier def test_no_supplier_matches_when_both_missing(self): """When neither PO nor supplier part can be resolved, no supplier_matches is set.""" url = reverse('api-barcode-po-receive') # Completely unknown barcode — no PO, no part, no supplier result = self.post( url, data={'barcode': 'COMPLETELY-UNKNOWN-BARCODE-XYZ'}, expected_code=400 ) self.assertIn('error', result.data) self.assertNotIn('supplier_matches', result.data) DIGIKEY_BARCODE = ( '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d' '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337' '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000' '00000000000000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000' ) # Uses 30P instead of P DIGIKEY_BARCODE_2 = ( '[)>\x1e06\x1d30P296-LM358BIDDFRCT-ND\x1dK\x1d1K72991337\x1d' '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337' '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000' '00000000000000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000' ) # Invalid code DIGIKEY_BARCODE_3 = ( '[)>\x1e06\x1dPnonsense\x1d30Pnonsense\x1d1Pnonsense\x1dK\x1d1K72991337\x1d' '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337' '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000' '00000000000000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000' ) MOUSER_BARCODE = ( '[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4' 'LMX\x1d1VTI\x1e\x04' ) MOUSER_BARCODE_OLD = ( '>[)>06\x1dK21421337\x1d14K033\x1d1PLDK320ADU33R\x1dQ32\x1d11K060931337\x1d' '4LCN\x1d1VSTMicro' ) LCSC_BARCODE = ( '{pbn:PICK2009291337,on:SO2009291337,pc:C312270,pm:ST-1-102-A01-T000-RS,qty' ':2,mc:,cc:1,pdi:34421807}' ) TME_QRCODE = ( 'QTY:1 PN:WBP-302 PO:19361337/1 CPO:PO-2023-06-08-001337 MFR:WISHERENTERPRI' 'SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302' ) TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1' # DigiKey barcode: '1K72991337' matches purchase_order1 via supplier_reference, # but 'PNONEXISTENT-SKU' has no matching supplier part in the test database. DIGIKEY_BARCODE_PO_NO_PART = ( '[)>\x1e06\x1dPNONEXISTENT-SKU-XXXX\x1d1PNONEXISTENT-MPN\x1dK\x1d1K72991337\x1d' '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK' ) # DigiKey barcode: 'P296-LM358BIDDFRCT-ND' matches the existing supplier part, # but '1KBADORDER-XXXXX' matches no purchase order in the test database. DIGIKEY_BARCODE_PART_NO_PO = ( '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1KBADORDER-XXXXX\x1d' '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK' )