2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-12 03:28:37 +00:00

Update to /api/barcode/po-recieve/ error messages (#11369)

* Update Dockerfile

* Update Dockerfile

* Added More Info In Barcode API Scans

Added DebugResponse
Added Suppliers Tested
Added Suppliers Errors

* Add NotFound to base Response

* Added Simple Plugin Debug

Changed "NotFound" to 'No_Match'
different style for Debug response

* Added finishing touches for API Error

* clean up + comments

* Added No Supplier Plugin error

* fix style

* Formatting and fixing comments

Added more variables
updated formatting
Added more in depth explanation block for Debug response

* duplicate code fix

* fixed some variable names

* Small move of variables

* Updated To Nested Debug Dictionary

fixed some issues with comments made in original PR
added more comments and small adjustments
got rid of "line item" in debug response

* Added notes to documentation

* Update docs/docs/app/barcode.md

adding matmirs comments

Co-authored-by: Matthias Mair <code@mjmair.com>

* Updated JSON plugin debug name

also added 'plugin_slug' variable

* fix style

* DNE style fix

* Style fix(again)

* Add unit tests

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
Idea-Junkie
2026-05-31 04:45:23 -04:00
committed by GitHub
parent b2b476b570
commit 4b415cb8ae
4 changed files with 207 additions and 15 deletions
@@ -533,10 +533,23 @@ class BarcodePOReceive(BarcodeView):
# Now, look just for "supplier-barcode" plugins
plugins = registry.with_mixin(PluginMixinEnum.SUPPLIER_BARCODE)
plugin_slug = None
plugin_response = None
plugin_error = None
no_supplier_plugin_error = []
supplier_purchase_order = None
plugin_supplier = None
supplier_part = None
for current_plugin in plugins:
try:
# Will either Output Debugresponse if No_Match is True or return the regular response if No_Match is False
result = current_plugin.scan_receive_item(
barcode,
request.user,
@@ -546,12 +559,58 @@ class BarcodePOReceive(BarcodeView):
line_item=line_item,
auto_allocate=auto_allocate,
)
except Exception:
log_error('BarcodePOReceive.handle_barcode', plugin=current_plugin.slug)
continue
if result is None:
continue
no_match = result.get('no_match', True)
# No_Match Determines if it found a exact match for all the required fields from scan_recieve_item
if no_match is True:
supplier_found = False
try:
plugin_slug = current_plugin.slug
supplier_purchase_order = result.get('PO')
plugin_supplier = result.get('supplier')
supplier_part = result.get('supplier_part')
except KeyError as e:
log_error(
f'BarcodePOReceive.handle_barcode debugresponse: KeyError {e}'
)
continue
# Supplier does not have associated Supplier ID
if plugin_supplier is None:
no_supplier_plugin_error.append(plugin_slug)
continue
# No Purchase Order or Supplier Part Found
if supplier_purchase_order is None and supplier_part is None:
continue
# Purchase Order exists and is found but Supplier part does not exist
if supplier_purchase_order != None and supplier_part is None:
# Supplier was Found
supplier_found = True
plugin_error = _('Purchase order Found\rNo supplier Part Match')
# Supplier Part is Found but Purchase Order does not exist
elif supplier_purchase_order is None and supplier_part != None:
# Supplier was Found
supplier_found = True
plugin_error = _('Supplier Part Found\rNo Purchase Order Match')
# Supplier for PO or Supplier part in barcode was found
if supplier_found is True:
# Adds info on for what was found in the barcode
response['supplier_matches'] = {
'purchase_order': supplier_purchase_order,
'no_match': no_match,
'supplier': plugin_supplier,
'supplier_part': supplier_part,
}
if 'error' in result:
logger.info(
@@ -569,13 +628,20 @@ class BarcodePOReceive(BarcodeView):
response['plugin'] = plugin.name if plugin else None
if plugin_response:
# If there is a plugin response, and there is a match (no_match = false), combine the dictionaries
if plugin_response and plugin_response.get('no_match') is False:
response = {**response, **plugin_response}
elif no_supplier_plugin_error:
response['no_supplier_plugin_error'] = no_supplier_plugin_error
# A plugin has not been found!
if plugin is None:
response['error'] = _('No plugin match for supplier barcode')
# A plugin was found, with a Error
elif plugin_error:
response['error'] = plugin_error
self.log_scan(request, response, 'success' in response)
if 'error' in response:
@@ -320,7 +320,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
location=None,
auto_allocate: bool = True,
**kwargs,
) -> dict | None:
) -> dict:
"""Attempt to receive an item against a PurchaseOrder via barcode scanning.
Arguments:
@@ -344,32 +344,50 @@ class SupplierBarcodeMixin(BarcodeMixin):
# Extract supplier information
supplier = supplier or self.get_supplier(cache=True)
if not supplier:
"""Construct Debug Response
This is returned if a perfect match is not found with the info provided from the barcode
Response Info:
'supplier': get supplier ID
'PO': Represented for "Purchase Order", find PO number to supplier
'supplier_part': find supplier part info to supplier
'no_match': Boolean, did we find a perfect match with info given? False is Yes, True is No
"""
debug_response = {}
if supplier is None:
# No supplier information available
return None
debug_response['supplier'] = None
else:
debug_response['supplier'] = supplier.name
# Extract purchase order information
purchase_order = purchase_order or self.get_purchase_order()
if not purchase_order or purchase_order.supplier != supplier:
if purchase_order is None or purchase_order.supplier != supplier:
# Purchase order does not match supplier
return None
debug_response['PO'] = None
else:
debug_response['PO'] = purchase_order.reference
supplier_part = self.get_supplier_part()
if not supplier_part:
if supplier_part is None:
# No supplier part information available
return None
debug_response['supplier_part'] = None
else:
debug_response['supplier_part'] = str(supplier_part.part)
# Attempt to find matching line item
if not line_item:
if not line_item and purchase_order != None:
line_items = purchase_order.lines.filter(part=supplier_part)
if line_items.count() == 1:
line_item = line_items.first()
if not line_item:
# No line item information available
return None
# If Purchase Order or Supplier Part does not exist, throw debug response
if debug_response['PO'] is None or debug_response['supplier_part'] is None:
debug_response['no_match'] = True
return debug_response
if line_item.part != supplier_part:
return {'error': _('Supplier part does not match line item')}
@@ -406,7 +424,8 @@ class SupplierBarcodeMixin(BarcodeMixin):
'supplier_part': supplier_part.pk,
'purchase_order': purchase_order.pk,
'location': location.pk if location else None,
}
},
'no_match': False,
}
if action_required:
@@ -428,6 +428,95 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
# 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'
@@ -476,3 +565,17 @@ TME_QRCODE = (
)
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'
)