mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-12 11:38:47 +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:
@@ -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'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user